sharetribe/sharetribe

View on GitHub
app/controllers/admin/custom_fields_controller.rb

Summary

Maintainability
A
3 hrs
Test Coverage
class Admin::CustomFieldsController < Admin::AdminBaseController

  before_action :field_type_is_valid, :only => [:new, :create]

  CHECKBOX_TO_BOOLEAN = ->(v) {
    if v == false || v == true
      v
    else
      v == "1"
    end
  }

  HASH_VALUES = ->(v) {
    if v.is_a?(Array)
      v
    elsif v.is_a?(Hash)
      v.values
    elsif v == nil
      nil
    else
      raise ArgumentError.new("Illegal argument given to transformer: #{v.to_inspect}")
    end
  }

  CategoryAttributeSpec = EntityUtils.define_builder(
    [:category_id, :fixnum, :to_integer, :mandatory]
  )

  OptionAttribute = EntityUtils.define_builder(
    [:id, :mandatory],
    [:sort_priority, :fixnum, :to_integer, :mandatory],
    [:title_attributes, :hash, :to_hash, :mandatory]
  )

  CUSTOM_FIELD_SPEC = [
    [:name_attributes, :hash, :mandatory],
    [:category_attributes, collection: CategoryAttributeSpec],
    [:sort_priority, :fixnum, :optional],
    [:required, :bool, :optional, default: false, transform_with: CHECKBOX_TO_BOOLEAN],
    [:search_filter, :bool, :optional, default: false, transform_with: CHECKBOX_TO_BOOLEAN]
  ]

  TextFieldSpec = [
    [:search_filter, :bool, const_value: false]
  ] + CUSTOM_FIELD_SPEC

  NumericFieldSpec = [
    [:min, :mandatory],
    [:max, :mandatory],
    [:allow_decimals, :bool, :mandatory, transform_with: CHECKBOX_TO_BOOLEAN],
    [:search_filter, :bool, :optional, default: false, transform_with: CHECKBOX_TO_BOOLEAN]
  ] + CUSTOM_FIELD_SPEC

  DropdownFieldSpec = [
    [:option_attributes, :mandatory, transform_with: HASH_VALUES, collection: OptionAttribute],
    [:search_filter, :bool, :optional, default: false, transform_with: CHECKBOX_TO_BOOLEAN],
  ] + CUSTOM_FIELD_SPEC

  CheckboxFieldSpec = [
    [:option_attributes, :mandatory, transform_with: HASH_VALUES, collection: OptionAttribute],
    [:search_filter, :bool, :optional, default: false, transform_with: CHECKBOX_TO_BOOLEAN]
  ] + CUSTOM_FIELD_SPEC

  DateFieldSpec = [
    [:search_filter, :bool, const_value: false]
  ] + CUSTOM_FIELD_SPEC

  TextFieldEntity     = EntityUtils.define_builder(*TextFieldSpec)
  NumericFieldEntity  = EntityUtils.define_builder(*NumericFieldSpec)
  DropdownFieldEntity = EntityUtils.define_builder(*DropdownFieldSpec)
  CheckboxFieldEntity = EntityUtils.define_builder(*CheckboxFieldSpec)
  DateFieldEntity     = EntityUtils.define_builder(*DateFieldSpec)

  def index
    @selected_left_navi_link = "listing_fields"
    @community = @current_community
    @custom_fields = @current_community.custom_fields

    shapes = @current_community.shapes
    price_in_use = shapes.any? { |s| s[:price_enabled] }

    make_onboarding_popup
    render locals: { show_price_filter: price_in_use }
  end

  def new
    @selected_left_navi_link = "listing_fields"
    @community = @current_community
    @custom_field = params[:field_type].constantize.new #before filter checks valid field types and prevents code injection

    if params[:field_type] == "CheckboxField"
      @min_option_count = 1
      @custom_field.options = [CustomFieldOption.new(sort_priority: 1)]
    else
      @min_option_count = 2
      @custom_field.options = [CustomFieldOption.new(sort_priority: 1), CustomFieldOption.new(sort_priority: 2)]
    end
  end

  def create
    @selected_left_navi_link = "listing_fields"
    @community = @current_community

    # Hack for comma/dot issue. Consider creating an app-wide comma/dot handling mechanism
    params[:custom_field][:min] = ParamsService.parse_float(params[:custom_field][:min]) if params[:custom_field][:min].present?
    params[:custom_field][:max] = ParamsService.parse_float(params[:custom_field][:max]) if params[:custom_field][:max].present?

    custom_field_entity = build_custom_field_entity(params[:field_type], params[:custom_field])

    @custom_field = params[:field_type].constantize.new(custom_field_entity) #before filter checks valid field types and prevents code injection
    @custom_field.entity_type = :for_listing
    @custom_field.community = @current_community

    success =
      if valid_categories?(@current_community, params[:custom_field][:category_attributes])
        @custom_field.save
      else
        false
      end

    if success
      # Onboarding wizard step recording
      state_changed = Admin::OnboardingWizard.new(@current_community.id)
        .update_from_event(:custom_field_created, @custom_field)
      if state_changed
        record_event(flash, "km_record", {km_event: "Onboarding filter created"})
        flash[:show_onboarding_popup] = true
      end

      redirect_to admin_custom_fields_path
    else
      flash[:error] = "Listing field saving failed"
      render :new
    end
  end

  def build_custom_field_entity(type, params)
    params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params
    case type
    when "TextField"
      TextFieldEntity.call(params)
    when "NumericField"
      NumericFieldEntity.call(params)
    when "DropdownField"
      DropdownFieldEntity.call(params)
    when "CheckboxField"
      CheckboxFieldEntity.call(params)
    when "DateField"
      DateFieldEntity.call(params)
    end
  end

  def edit
    @selected_tribe_navi_tab = "admin"
    @selected_left_navi_link = "listing_fields"
    @community = @current_community

    if params[:field_type] == "CheckboxField"
      @min_option_count = 1
    else
      @min_option_count = 2
    end

    @custom_field = @current_community.custom_fields.find(params[:id])
  end

  def update
    @custom_field = @current_community.custom_fields.find(params[:id])

    # Hack for comma/dot issue. Consider creating an app-wide comma/dot handling mechanism
    params[:custom_field][:min] = ParamsService.parse_float(params[:custom_field][:min]) if params[:custom_field][:min].present?
    params[:custom_field][:max] = ParamsService.parse_float(params[:custom_field][:max]) if params[:custom_field][:max].present?

    custom_field_params = params[:custom_field].merge(
      sort_priority: @custom_field.sort_priority
    )

    custom_field_entity = build_custom_field_entity(@custom_field.type, custom_field_params)

    @custom_field.update(custom_field_entity)

    redirect_to admin_custom_fields_path
  end

  def edit_price
    @selected_tribe_navi_tab = "admin"
    @selected_left_navi_link = "listing_fields"
    @community = @current_community
  end

  def edit_location
    @selected_tribe_navi_tab = "admin"
    @selected_left_navi_link = "listing_fields"
    @community = @current_community
  end

  def edit_expiration
    @selected_tribe_navi_tab = "admin"
    @selected_left_navi_link = "listing_fields"
    @community = @current_community

    render_expiration_form(listing_expiration_enabled: !@current_community.hide_expiration_date)
  end

  def update_price
    # To cents
    params[:community][:price_filter_min] = MoneyUtil.parse_str_to_money(params[:community][:price_filter_min], @current_community.currency).cents if params[:community][:price_filter_min]
    params[:community][:price_filter_max] = MoneyUtil.parse_str_to_money(params[:community][:price_filter_max], @current_community.currency).cents if params[:community][:price_filter_max]

    price_params = params.require(:community).permit(
      :show_price_filter,
      :price_filter_min,
      :price_filter_max
    )

    success = @current_community.update(price_params)

    if success
      redirect_to admin_custom_fields_path
    else
      flash[:error] = "Price field editing failed"
      render :action => :edit_price
    end
  end

  def update_location
    location_params = params.require(:community).permit(:listing_location_required)

    success = @current_community.update(location_params)

    if success
      redirect_to admin_custom_fields_path
    else
      flash[:error] = "Location field editing failed"
      render :action => :edit_location
    end
  end

  def update_expiration
    listing_expiration_enabled = params[:listing_expiration_enabled] == "enabled"

    success = @current_community.update(
      { hide_expiration_date: !listing_expiration_enabled })

    if success
      redirect_to admin_custom_fields_path
    else
      flash[:error] = "Expiration field editing failed"
      render_expiration_form(listing_expiration_enabled: !@current_community.hide_expiration_date)
    end
  end

  def render_expiration_form(listing_expiration_enabled:)
    render :edit_expiration, locals: {
             listing_expiration_enabled: listing_expiration_enabled
           }
  end

  def destroy
    @custom_field = CustomField.find(params[:id])

    success = if custom_field_belongs_to_community?(@custom_field, @current_community)
      @custom_field.destroy
    end

    flash[:error] = "Field doesn't belong to current community" unless success
    redirect_to admin_custom_fields_path
  end

  def order
    sort_priorities = params[:order].each_with_index.map do |custom_field_id, index|
      [custom_field_id, index]
    end.inject({}) do |hash, ids|
      custom_field_id, sort_priority = ids
      hash.merge(custom_field_id.to_i => sort_priority)
    end

    @current_community.custom_fields.each do |custom_field|
      custom_field.update(:sort_priority => sort_priorities[custom_field.id])
    end

    render body: nil, status: :ok
  end

  private

  # Return `true` if all the category id's belong to `community`
  def valid_categories?(community, category_attributes)
    is_community_category = category_attributes.map do |category|
      community.categories.any? { |community_category| community_category.id == category[:category_id].to_i }
    end

    is_community_category.all?
  end

  def custom_field_belongs_to_community?(custom_field, community)
    community.custom_fields.include?(custom_field)
  end

  private

  def field_type_is_valid
    redirect_to admin_custom_fields_path unless CustomField::VALID_TYPES.include?(params[:field_type])
  end

end