diff --git a/app/controllers/concerns/products/ajax_search.rb b/app/controllers/admin/ajax_search_controller.rb similarity index 89% rename from app/controllers/concerns/products/ajax_search.rb rename to app/controllers/admin/ajax_search_controller.rb index 37cec78e0a..108037873f 100644 --- a/app/controllers/concerns/products/ajax_search.rb +++ b/app/controllers/admin/ajax_search_controller.rb @@ -1,23 +1,21 @@ # frozen_string_literal: true -module Products - module AjaxSearch - extend ActiveSupport::Concern - - def search_producers +module Admin + class AjaxSearchController < Spree::Admin::BaseController + def producers query = OpenFoodNetwork::Permissions.new(spree_current_user) .managed_product_enterprises.is_primary_producer.by_name render json: build_search_response(query) end - def search_categories + def categories query = Spree::Taxon.all render json: build_search_response(query) end - def search_tax_categories + def tax_categories query = Spree::TaxCategory.all render json: build_search_response(query) diff --git a/app/controllers/admin/products_v3_controller.rb b/app/controllers/admin/products_v3_controller.rb index ff3bc8d433..80b99aed73 100644 --- a/app/controllers/admin/products_v3_controller.rb +++ b/app/controllers/admin/products_v3_controller.rb @@ -4,7 +4,6 @@ module Admin class ProductsV3Controller < Spree::Admin::BaseController helper ProductsHelper - include ::Products::AjaxSearch before_action :init_filters_params before_action :init_pagination_params diff --git a/app/models/spree/ability.rb b/app/models/spree/ability.rb index 0de27a0718..8a41422713 100644 --- a/app/models/spree/ability.rb +++ b/app/models/spree/ability.rb @@ -228,12 +228,11 @@ module Spree :destroy, :destroy_variant, :clone, - :create_linked_variant, - :search_producers, - :search_categories, - :search_tax_categories + :create_linked_variant ], :products_v3 + can [:admin, :producers, :categories, :tax_categories], :ajax_search + can [:create], Spree::Variant can [:admin, :index, :read, :edit, :update, :search, :delete, :destroy], Spree::Variant do |variant| diff --git a/app/views/admin/products_v3/_filters.html.haml b/app/views/admin/products_v3/_filters.html.haml index bf82a02c72..96ace78b82 100644 --- a/app/views/admin/products_v3/_filters.html.haml +++ b/app/views/admin/products_v3/_filters.html.haml @@ -13,7 +13,7 @@ aria_label: t('.producers.label'), options: selected_option(producer_id, Enterprise), selected_option: producer_id, - remote_url: admin_products_search_producers_url, + remote_url: admin_ajax_search_producers_url, include_blank: t('.all_producers'), placeholder_value: t('.search_for_producers'))) .categories @@ -22,7 +22,7 @@ aria_label: t('.categories.label'), options: selected_option(category_id, Spree::Taxon), selected_option: category_id, - remote_url: admin_products_search_categories_url, + remote_url: admin_ajax_search_categories_url, include_blank: t('.all_categories'), placeholder_value: t('.search_for_categories'))) -if variant_tag_enabled?(spree_current_user) diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 7bc193f1e4..139e2bef7f 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -61,7 +61,7 @@ aria_label: t('.producer_field_name'), options: variant.supplier_id ? [[variant.supplier.name, variant.supplier_id]] : [], selected_option: variant.supplier_id, - remote_url: admin_products_search_producers_url, + remote_url: admin_ajax_search_producers_url, placeholder_value: t('admin.products_v3.filters.select_producer'))) = error_message_on variant, :supplier %td.col-category.field.naked_inputs @@ -70,7 +70,7 @@ options: variant.primary_taxon_id ? [[variant.primary_taxon.name, variant.primary_taxon_id]] : [], selected_option: variant.primary_taxon_id, aria_label: t('.category_field_name'), - remote_url: admin_products_search_categories_url, + remote_url: admin_ajax_search_categories_url, placeholder_value: t('admin.products_v3.filters.select_category'))) = error_message_on variant, :primary_taxon %td.col-tax_category.field.naked_inputs @@ -80,7 +80,7 @@ selected_option: variant.tax_category_id, aria_label: t('.tax_category_field_name'), include_blank: t('.none_tax_category'), - remote_url: admin_products_search_tax_categories_url, + remote_url: admin_ajax_search_tax_categories_url, placeholder_value: t('.search_for_tax_categories'))) = error_message_on variant, :tax_category - if variant_tag_enabled?(spree_current_user) diff --git a/bin/setup b/bin/setup index a55bfa66b9..f1e5fa0be5 100755 --- a/bin/setup +++ b/bin/setup @@ -27,7 +27,7 @@ FileUtils.chdir APP_ROOT do system("bundle check 2> /dev/null") || system!(BUNDLE_ENV, "bundle install") # Install JavaScript dependencies - system!("script/nodenv-install.sh") + system("script/nodenv-install.sh") system!("bin/yarn") # puts "\n== Copying sample files ==" diff --git a/config/routes/admin.rb b/config/routes/admin.rb index bfc9434a41..dd46ab70c0 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -83,9 +83,13 @@ Openfoodnetwork::Application.routes.draw do delete 'products_v3/destroy_variant/:id', to: 'products_v3#destroy_variant', as: 'destroy_variant' post 'clone/:id', to: 'products_v3#clone', as: 'clone_product' post 'products/create_linked_variant', to: 'products_v3#create_linked_variant', as: 'create_linked_variant' - get 'products_v3/search_producers', to: 'products_v3#search_producers', as: 'products_search_producers' - get 'products_v3/search_categories', to: 'products_v3#search_categories', as: 'products_search_categories' - get 'products_v3/search_tax_categories', to: 'products_v3#search_tax_categories', as: 'products_search_tax_categories' + + scope :ajax_search, as: :ajax_search, controller: :ajax_search do + get :producers + get :categories + get :tax_categories + end + resources :product_preview, only: [:show] resources :variant_overrides do diff --git a/spec/requests/admin/ajax_search_controller_spec.rb b/spec/requests/admin/ajax_search_controller_spec.rb new file mode 100644 index 0000000000..b23f59efb4 --- /dev/null +++ b/spec/requests/admin/ajax_search_controller_spec.rb @@ -0,0 +1,285 @@ +# frozen_string_literal: true + +RSpec.describe "Admin::AjaxSearch" do + include AuthenticationHelper + + let(:admin_user) { create(:admin_user) } + let(:regular_user) { create(:user) } + + describe "GET /admin/ajax_search/producers" do + context "when user is not logged in" do + it "redirects to login" do + get admin_ajax_search_producers_path + + expect(response).to redirect_to %r|#/login$| + end + end + + context "when user is logged in without permissions" do + before { login_as regular_user } + + it "redirects to unauthorized" do + get admin_ajax_search_producers_path + + expect(response).to redirect_to('/unauthorized') + end + end + + context "when user is an admin" do + before { login_as admin_user } + + let!(:producer1) { create(:supplier_enterprise, name: "Apple Farm") } + let!(:producer2) { create(:supplier_enterprise, name: "Berry Farm") } + let!(:producer3) { create(:supplier_enterprise, name: "Cherry Orchard") } + let!(:distributor) { create(:distributor_enterprise, name: "Distributor") } + + it "returns producers sorted alphabetically by name" do + get admin_ajax_search_producers_path + + expect(response).to have_http_status(:ok) + json_response = response.parsed_body + + expect(json_response["results"].pluck("label")).to eq(['Apple Farm', 'Berry Farm', + 'Cherry Orchard']) + expect(json_response["pagination"]["more"]).to be false + end + + it "filters producers by search query" do + get admin_ajax_search_producers_path, params: { q: "berry" } + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['Berry Farm']) + expect(json_response["results"].pluck("value")).to eq([producer2.id]) + end + + it "filters are case insensitive" do + get admin_ajax_search_producers_path, params: { q: "BERRY" } + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['Berry Farm']) + end + + it "filters with partial matches" do + get admin_ajax_search_producers_path, params: { q: "Farm" } + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['Apple Farm', 'Berry Farm']) + end + + it "excludes non-producer enterprises" do + get admin_ajax_search_producers_path + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).not_to include('Distributor') + end + + context "with more than 30 producers" do + before do + create_list(:supplier_enterprise, 35) do |enterprise, i| + enterprise.update!(name: "Producer #{(i + 1).to_s.rjust(2, '0')}") + end + end + + it "returns first page with 30 results and more flag as true" do + get admin_ajax_search_producers_path, params: { page: 1 } + + json_response = response.parsed_body + expect(json_response["results"].length).to eq(30) + expect(json_response["pagination"]["more"]).to be true + end + + it "returns remaining results on second page with more flag as false" do + get admin_ajax_search_producers_path, params: { page: 2 } + + json_response = response.parsed_body + expect(json_response["results"].length).to eq(8) + expect(json_response["pagination"]["more"]).to be false + end + end + end + + context "when user has enterprise permissions" do + let!(:my_producer) { create(:supplier_enterprise, name: "My Producer") } + let!(:other_producer) { create(:supplier_enterprise, name: "Other Producer") } + let(:user_with_producer) { create(:user, enterprises: [my_producer]) } + + before { login_as user_with_producer } + + it "returns only managed producers" do + get admin_ajax_search_producers_path + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['My Producer']) + expect(json_response["results"].pluck("label")).not_to include('Other Producer') + end + end + end + + describe "GET /admin/ajax_search/categories" do + context "when user is not logged in" do + it "redirects to login" do + get admin_ajax_search_categories_path + + expect(response).to redirect_to %r|#/login$| + end + end + + context "when user is logged in without permissions" do + before { login_as regular_user } + + it "redirects to unauthorized" do + get admin_ajax_search_categories_path + + expect(response).to redirect_to('/unauthorized') + end + end + + context "when user is an admin" do + before { login_as admin_user } + + let!(:category1) { create(:taxon, name: "Vegetables") } + let!(:category2) { create(:taxon, name: "Fruits") } + let!(:category3) { create(:taxon, name: "Dairy") } + + it "returns categories sorted alphabetically by name" do + get admin_ajax_search_categories_path + + expect(response).to have_http_status(:ok) + json_response = response.parsed_body + + expect(json_response["results"].pluck("label")).to eq(['Dairy', 'Fruits', 'Vegetables']) + expect(json_response["pagination"]["more"]).to be false + end + + it "filters categories by search query" do + get admin_ajax_search_categories_path, params: { q: "fruit" } + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['Fruits']) + expect(json_response["results"].pluck("value")).to eq([category2.id]) + end + + it "filters are case insensitive" do + get admin_ajax_search_categories_path, params: { q: "VEGETABLES" } + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['Vegetables']) + end + + it "filters with partial matches" do + get admin_ajax_search_categories_path, params: { q: "ege" } + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['Vegetables']) + end + + context "with more than 30 categories" do + before do + create_list(:taxon, 35) do |taxon, i| + taxon.update!(name: "Category #{(i + 1).to_s.rjust(2, '0')}") + end + end + + it "returns first page with 30 results and more flag as true" do + get admin_ajax_search_categories_path, params: { page: 1 } + + json_response = response.parsed_body + expect(json_response["results"].length).to eq(30) + expect(json_response["pagination"]["more"]).to be true + end + + it "returns remaining results on second page with more flag as false" do + get admin_ajax_search_categories_path, params: { page: 2 } + + json_response = response.parsed_body + expect(json_response["results"].length).to eq(8) + expect(json_response["pagination"]["more"]).to be false + end + end + end + end + + describe "GET /admin/ajax_search/tax_categories" do + context "when user is not logged in" do + it "redirects to login" do + get admin_ajax_search_tax_categories_path + + expect(response).to redirect_to %r|#/login$| + end + end + + context "when user is logged in without permissions" do + before { login_as regular_user } + + it "redirects to unauthorized" do + get admin_ajax_search_tax_categories_path + + expect(response).to redirect_to('/unauthorized') + end + end + + context "when user is an admin" do + before { login_as admin_user } + + let!(:tax_cat1) { create(:tax_category, name: "GST") } + let!(:tax_cat2) { create(:tax_category, name: "VAT") } + let!(:tax_cat3) { create(:tax_category, name: "No Tax") } + + it "returns tax categories sorted alphabetically by name" do + get admin_ajax_search_tax_categories_path + + expect(response).to have_http_status(:ok) + json_response = response.parsed_body + + expect(json_response["results"].pluck("label")).to eq(['GST', 'No Tax', 'VAT']) + expect(json_response["pagination"]["more"]).to be false + end + + it "filters tax categories by search query" do + get admin_ajax_search_tax_categories_path, params: { q: "vat" } + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['VAT']) + expect(json_response["results"].pluck("value")).to eq([tax_cat2.id]) + end + + it "filters are case insensitive" do + get admin_ajax_search_tax_categories_path, params: { q: "GST" } + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['GST']) + end + + it "filters with partial matches" do + get admin_ajax_search_tax_categories_path, params: { q: "tax" } + + json_response = response.parsed_body + expect(json_response["results"].pluck("label")).to eq(['No Tax']) + end + + context "with more than 30 tax categories" do + before do + create_list(:tax_category, 35) do |tax_cat, i| + tax_cat.update!(name: "Tax Category #{(i + 1).to_s.rjust(2, '0')}") + end + end + + it "returns first page with 30 results and more flag as true" do + get admin_ajax_search_tax_categories_path, params: { page: 1 } + + json_response = response.parsed_body + expect(json_response["results"].length).to eq(30) + expect(json_response["pagination"]["more"]).to be true + end + + it "returns remaining results on second page with more flag as false" do + get admin_ajax_search_tax_categories_path, params: { page: 2 } + + json_response = response.parsed_body + expect(json_response["results"].length).to eq(8) + expect(json_response["pagination"]["more"]).to be false + end + end + end + end +end