mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-04-02 06:51:40 +00:00
Compare commits
43 Commits
v5.4.8
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b88cf10da | ||
|
|
046bbc3cf8 | ||
|
|
2722112125 | ||
|
|
4d79dcdd2d | ||
|
|
846a59873e | ||
|
|
2601b55a6f | ||
|
|
190e9a79b2 | ||
|
|
ba3e9279bd | ||
|
|
26e0f4b09b | ||
|
|
91588ad94e | ||
|
|
fb73144172 | ||
|
|
d1c045cbf0 | ||
|
|
473fff1bb8 | ||
|
|
baf390eade | ||
|
|
450fe4ada1 | ||
|
|
bcecbf9a0f | ||
|
|
2e6e4b665f | ||
|
|
77b6bc15e7 | ||
|
|
9b145da898 | ||
|
|
00d600911d | ||
|
|
10d6dd73f2 | ||
|
|
c74624cd57 | ||
|
|
60edcada2c | ||
|
|
b61f6ab444 | ||
|
|
80a12db191 | ||
|
|
5beed6f028 | ||
|
|
0a65322594 | ||
|
|
b7f154d289 | ||
|
|
edb8a03436 | ||
|
|
3ee338fa8d | ||
|
|
e3da27ca12 | ||
|
|
4c8e6d8260 | ||
|
|
de28083007 | ||
|
|
01bfd72387 | ||
|
|
69d9c52a53 | ||
|
|
5371361a74 | ||
|
|
b7c628dc2a | ||
|
|
5bef61aa2e | ||
|
|
79c346acb1 | ||
|
|
ca10ae2f5c | ||
|
|
8ba0ab6b5a | ||
|
|
044f6131da | ||
|
|
062fcd317c |
6
.env
6
.env
@@ -21,12 +21,6 @@ CHECKOUT_ZONE="Australia"
|
||||
# Find currency codes at http://en.wikipedia.org/wiki/ISO_4217.
|
||||
CURRENCY="AUD"
|
||||
|
||||
# The whenever gem can set the `MAILTO` variable for our cron jobs.
|
||||
# You can define an email address to notify if any job outputs something.
|
||||
# But you need a working mail server setup so that the message is delivered.
|
||||
# See: config/schedule.rb
|
||||
# SCHEDULE_NOTIFICATIONS="admin@example.com"
|
||||
|
||||
# Mail settings
|
||||
MAIL_HOST="example.com"
|
||||
MAIL_DOMAIN="example.com"
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 1400 --no-auto-gen-timestamp`
|
||||
# using RuboCop version 1.81.7.
|
||||
# using RuboCop version 1.86.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 8
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
|
||||
# SupportedStylesAlignWith: start_of_line, relative_to_receiver
|
||||
Layout/IndentationWidth:
|
||||
Exclude:
|
||||
- 'app/models/spree/taxon.rb'
|
||||
- 'app/services/dfc_catalog_importer.rb'
|
||||
- 'lib/reporting/reports/customers/base.rb'
|
||||
- 'lib/reporting/reports/enterprise_fee_summary/enterprise_fees_with_tax_report_by_order.rb'
|
||||
- 'spec/models/spree/order/state_machine_spec.rb'
|
||||
- 'spec/services/orders/compare_invoice_service_spec.rb'
|
||||
- 'spec/system/admin/bulk_order_management_spec.rb'
|
||||
- 'spec/system/consumer/checkout/payment_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: RequireParenthesesForMethodChains.
|
||||
@@ -35,6 +50,7 @@ Lint/UselessConstantScoping:
|
||||
- 'lib/reporting/report_metadata_builder.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Lint/UselessOr:
|
||||
Exclude:
|
||||
- 'app/models/product_import/entry_validator.rb'
|
||||
@@ -62,14 +78,13 @@ Metrics/AbcSize:
|
||||
- 'lib/spree/core/controller_helpers/order.rb'
|
||||
- 'spec/services/orders/checkout_restart_service_spec.rb'
|
||||
|
||||
# Offense count: 9
|
||||
# Offense count: 7
|
||||
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
|
||||
# AllowedMethods: refine
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- 'app/models/spree/order/checkout.rb'
|
||||
- 'app/models/spree/payment.rb'
|
||||
- 'app/models/spree/payment/processing.rb'
|
||||
- 'app/models/spree/shipment.rb'
|
||||
- 'lib/spree/core/controller_helpers/common.rb'
|
||||
- 'lib/tasks/data.rake'
|
||||
@@ -80,7 +95,7 @@ Metrics/BlockNesting:
|
||||
Exclude:
|
||||
- 'app/models/spree/payment/processing.rb'
|
||||
|
||||
# Offense count: 49
|
||||
# Offense count: 48
|
||||
# Configuration parameters: CountComments, Max, CountAsOne.
|
||||
Metrics/ClassLength:
|
||||
Exclude:
|
||||
@@ -105,7 +120,6 @@ Metrics/ClassLength:
|
||||
- 'app/models/product_import/product_importer.rb'
|
||||
- 'app/models/spree/ability.rb'
|
||||
- 'app/models/spree/address.rb'
|
||||
- 'app/models/spree/credit_card.rb'
|
||||
- 'app/models/spree/gateway/stripe_sca.rb'
|
||||
- 'app/models/spree/line_item.rb'
|
||||
- 'app/models/spree/order.rb'
|
||||
@@ -134,11 +148,12 @@ Metrics/ClassLength:
|
||||
- 'lib/reporting/reports/enterprise_fee_summary/scope.rb'
|
||||
- 'lib/reporting/reports/xero_invoices/base.rb'
|
||||
|
||||
# Offense count: 37
|
||||
# Offense count: 33
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
||||
Metrics/CyclomaticComplexity:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/enterprises_controller.rb'
|
||||
- 'app/controllers/concerns/respond_with.rb'
|
||||
- 'app/controllers/spree/admin/payments_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/helpers/checkout_helper.rb'
|
||||
@@ -159,16 +174,12 @@ Metrics/CyclomaticComplexity:
|
||||
- 'lib/open_food_network/enterprise_issue_validator.rb'
|
||||
- 'lib/reporting/reports/orders_and_fulfillment/order_cycle_customer_totals.rb'
|
||||
- 'lib/reporting/reports/orders_and_fulfillment/order_cycle_supplier_totals.rb'
|
||||
- 'lib/reporting/reports/payments/itemised_payment_totals.rb'
|
||||
- 'lib/reporting/reports/payments/payment_totals.rb'
|
||||
- 'lib/reporting/reports/sales_tax/sales_tax_totals_by_producer.rb'
|
||||
- 'lib/reporting/reports/xero_invoices/base.rb'
|
||||
- 'lib/spree/core/controller_helpers/order.rb'
|
||||
- 'lib/spree/core/controller_helpers/respond_with.rb'
|
||||
- 'lib/spree/localized_number.rb'
|
||||
- 'spec/models/product_importer_spec.rb'
|
||||
|
||||
# Offense count: 22
|
||||
# Offense count: 20
|
||||
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
@@ -178,7 +189,6 @@ Metrics/MethodLength:
|
||||
- 'app/models/spree/ability.rb'
|
||||
- 'app/models/spree/gateway/pay_pal_express.rb'
|
||||
- 'app/models/spree/order/checkout.rb'
|
||||
- 'app/models/spree/payment/processing.rb'
|
||||
- 'app/models/spree/preferences/preferable_class_methods.rb'
|
||||
- 'lib/open_food_network/order_cycle_form_applicator.rb'
|
||||
- 'lib/open_food_network/order_cycle_permissions.rb'
|
||||
@@ -187,12 +197,11 @@ Metrics/MethodLength:
|
||||
- 'lib/spree/localized_number.rb'
|
||||
- 'lib/tasks/sample_data/product_factory.rb'
|
||||
|
||||
# Offense count: 10
|
||||
# Offense count: 9
|
||||
# Configuration parameters: CountComments, Max, CountAsOne.
|
||||
Metrics/ModuleLength:
|
||||
Exclude:
|
||||
- 'app/helpers/admin/injection_helper.rb'
|
||||
- 'app/helpers/checkout_helper.rb'
|
||||
- 'app/helpers/injection_helper.rb'
|
||||
- 'app/helpers/spree/admin/base_helper.rb'
|
||||
- 'app/helpers/spree/admin/navigation_helper.rb'
|
||||
@@ -238,7 +247,7 @@ Naming/MethodParameterName:
|
||||
# Offense count: 60
|
||||
# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
|
||||
# AllowedMethods: call
|
||||
# WaywardPredicates: nonzero?
|
||||
# WaywardPredicates: infinite?, nonzero?
|
||||
Naming/PredicateMethod:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/product_import_controller.rb'
|
||||
@@ -336,16 +345,14 @@ Rails/OrderArguments:
|
||||
- 'spec/services/orders/generate_invoice_service_spec.rb'
|
||||
- 'spec/system/admin/order_cycles/simple_spec.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Offense count: 1
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
Rails/Presence:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/enterprises_controller.rb'
|
||||
- 'app/models/spree/product.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: Severity.
|
||||
Rails/RedirectBackOrTo:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/order_cycles_controller.rb'
|
||||
@@ -373,7 +380,7 @@ Style/BitwisePredicate:
|
||||
Exclude:
|
||||
- 'app/helpers/admin/enterprises_helper.rb'
|
||||
|
||||
# Offense count: 23
|
||||
# Offense count: 22
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules.
|
||||
# SupportedStyles: nested, compact
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -118,8 +118,6 @@ gem 'immigrant'
|
||||
gem 'roo' # read spreadsheets
|
||||
gem 'spreadsheet_architect' # write spreadsheets
|
||||
|
||||
gem 'whenever', require: false
|
||||
|
||||
gem 'coffee-rails', '~> 5.0.0'
|
||||
|
||||
gem 'angular_rails_csrf'
|
||||
|
||||
28
Gemfile.lock
28
Gemfile.lock
@@ -112,7 +112,7 @@ GEM
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_model_serializers (0.8.4)
|
||||
activemodel (>= 3.0)
|
||||
active_storage_validations (3.0.3)
|
||||
active_storage_validations (3.0.4)
|
||||
activejob (>= 6.1.4)
|
||||
activemodel (>= 6.1.4)
|
||||
activestorage (>= 6.1.4)
|
||||
@@ -210,7 +210,7 @@ GEM
|
||||
bigdecimal (3.3.1)
|
||||
bindata (2.5.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.22.0)
|
||||
bootsnap (1.23.0)
|
||||
msgpack (~> 1.2)
|
||||
bugsnag (6.29.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@@ -245,7 +245,6 @@ GEM
|
||||
cgi (0.5.1)
|
||||
childprocess (5.0.0)
|
||||
choice (0.2.0)
|
||||
chronic (0.10.2)
|
||||
coderay (1.1.3)
|
||||
coffee-rails (5.0.0)
|
||||
coffee-script (>= 2.2.0)
|
||||
@@ -290,8 +289,8 @@ GEM
|
||||
warden (~> 1.2.3)
|
||||
devise-encryptable (0.2.0)
|
||||
devise (>= 2.1.0)
|
||||
devise-i18n (1.15.0)
|
||||
devise (>= 4.9.0)
|
||||
devise-i18n (1.16.0)
|
||||
devise (>= 5.0.0)
|
||||
rails-i18n
|
||||
diff-lcs (1.6.2)
|
||||
digest (3.2.1)
|
||||
@@ -577,7 +576,7 @@ GEM
|
||||
parallel (1.27.0)
|
||||
paranoia (2.6.4)
|
||||
activerecord (>= 5.1, < 7.2)
|
||||
parser (3.3.10.2)
|
||||
parser (3.3.11.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
paypal-sdk-core (0.3.4)
|
||||
@@ -591,7 +590,7 @@ GEM
|
||||
hashery (~> 2.0)
|
||||
ruby-rc4
|
||||
ttfunk
|
||||
pg (1.6.2)
|
||||
pg (1.6.3)
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
@@ -777,7 +776,7 @@ GEM
|
||||
rswag-ui (2.17.0)
|
||||
actionpack (>= 5.2, < 8.2)
|
||||
railties (>= 5.2, < 8.2)
|
||||
rubocop (1.84.2)
|
||||
rubocop (1.86.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -788,7 +787,7 @@ GEM
|
||||
rubocop-ast (>= 1.49.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.49.0)
|
||||
rubocop-ast (1.49.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.7)
|
||||
rubocop-capybara (2.22.1)
|
||||
@@ -905,7 +904,7 @@ GEM
|
||||
thor (1.5.0)
|
||||
thread-local (1.1.0)
|
||||
tilt (2.7.0)
|
||||
timeout (0.6.0)
|
||||
timeout (0.6.1)
|
||||
tsort (0.2.0)
|
||||
ttfunk (1.8.0)
|
||||
bigdecimal (~> 3.1)
|
||||
@@ -938,9 +937,9 @@ GEM
|
||||
validates_lengths_from_database (0.8.0)
|
||||
activerecord (>= 4)
|
||||
vcr (6.4.0)
|
||||
view_component (4.1.1)
|
||||
actionview (>= 7.1.0, < 8.2)
|
||||
activesupport (>= 7.1.0, < 8.2)
|
||||
view_component (4.5.0)
|
||||
actionview (>= 7.1.0)
|
||||
activesupport (>= 7.1.0)
|
||||
concurrent-ruby (~> 1)
|
||||
view_component_reflex (3.1.14.pre9)
|
||||
rails (>= 5.2, < 8.0)
|
||||
@@ -968,8 +967,6 @@ GEM
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
whenever (1.1.0)
|
||||
chronic (>= 0.6.3)
|
||||
xml-simple (1.1.8)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
@@ -1127,7 +1124,6 @@ DEPENDENCIES
|
||||
web!
|
||||
web-console
|
||||
webmock
|
||||
whenever
|
||||
wicked_pdf!
|
||||
wkhtmltopdf-binary!
|
||||
|
||||
|
||||
56
app/controllers/admin/ajax_search_controller.rb
Normal file
56
app/controllers/admin/ajax_search_controller.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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 categories
|
||||
query = Spree::Taxon.all
|
||||
|
||||
render json: build_search_response(query)
|
||||
end
|
||||
|
||||
def tax_categories
|
||||
query = Spree::TaxCategory.all
|
||||
|
||||
render json: build_search_response(query)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_search_response(query)
|
||||
page = (params[:page] || 1).to_i
|
||||
per_page = 30
|
||||
|
||||
filtered_query = apply_search_filter(query)
|
||||
total_count = filtered_query.size
|
||||
items = paginated_items(filtered_query, page, per_page)
|
||||
results = format_results(items)
|
||||
|
||||
{ results: results, pagination: { more: (page * per_page) < total_count } }
|
||||
end
|
||||
|
||||
def apply_search_filter(query)
|
||||
search_term = params[:q]
|
||||
return query if search_term.blank?
|
||||
|
||||
escaped_search_term = ActiveRecord::Base.sanitize_sql_like(search_term)
|
||||
pattern = "%#{escaped_search_term}%"
|
||||
|
||||
query.where('name ILIKE ?', pattern)
|
||||
end
|
||||
|
||||
def paginated_items(query, page, per_page)
|
||||
query.order(:name).offset((page - 1) * per_page).limit(per_page).pluck(:name, :id)
|
||||
end
|
||||
|
||||
def format_results(items)
|
||||
items.map { |label, value| { value:, label: } }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base
|
||||
include CablecarResponses
|
||||
include Pagy::Backend
|
||||
include RequestTimeouts
|
||||
include RespondWith
|
||||
|
||||
self.responder = ApplicationResponder
|
||||
respond_to :html
|
||||
|
||||
@@ -44,6 +44,7 @@ module CheckoutCallbacks
|
||||
@order.checkout_processing = true
|
||||
|
||||
redirect_to(main_app.shop_path) && return if redirect_to_shop?
|
||||
|
||||
redirect_to_cart_path && return unless valid_order_line_items?
|
||||
end
|
||||
|
||||
|
||||
49
app/controllers/concerns/respond_with.rb
Normal file
49
app/controllers/concerns/respond_with.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spree/responder"
|
||||
|
||||
module RespondWith
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def respond_with(*resources, &)
|
||||
if self.class.mimes_for_respond_to.empty?
|
||||
raise "In order to use respond_with, first you need to declare the formats your " \
|
||||
"controller responds to in the class level"
|
||||
end
|
||||
|
||||
return unless (collector = retrieve_collector_from_mimes(&))
|
||||
|
||||
options = resources.size == 1 ? {} : resources.extract_options!
|
||||
|
||||
# Fix spree issues #3531 and #2210 (patch provided by leiyangyou)
|
||||
if (defined_response = collector.response) &&
|
||||
!ApplicationController.spree_responders[self.class.to_s.to_sym]
|
||||
.try(:[], action_name.to_sym)
|
||||
|
||||
if action = options.delete(:action)
|
||||
render(action:)
|
||||
else
|
||||
defined_response.call
|
||||
end
|
||||
else
|
||||
# The action name is needed for processing
|
||||
options[:action_name] = action_name.to_sym
|
||||
# If responder is not specified then pass in Spree::Responder
|
||||
(options.delete(:responder) || Spree::Responder).call(self, resources, options)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def retrieve_collector_from_mimes(mimes = nil, &block)
|
||||
mimes ||= collect_mimes_from_class_level
|
||||
collector = ActionController::Base::Collector.new(mimes, request.variant)
|
||||
block.call(collector) if block_given?
|
||||
format = collector.negotiate_format(request)
|
||||
|
||||
raise ActionController::UnknownFormat unless format
|
||||
|
||||
_process_format(format)
|
||||
collector
|
||||
end
|
||||
end
|
||||
@@ -52,5 +52,15 @@ module Admin
|
||||
@allowed_source_producers ||= OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
.enterprises_granting_linked_variants
|
||||
end
|
||||
|
||||
# Query only name of the model to avoid loading the whole record
|
||||
def selected_option(id, model)
|
||||
return [] unless id
|
||||
|
||||
name = model.where(id: id).pick(:name)
|
||||
return [] unless name
|
||||
|
||||
[[name, id]]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
15
app/jobs/rake_job.rb
Normal file
15
app/jobs/rake_job.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rake"
|
||||
|
||||
# Executes a rake task
|
||||
class RakeJob < ApplicationJob
|
||||
def perform(task_string)
|
||||
Rails.application.load_tasks if Rake::Task.tasks.empty?
|
||||
|
||||
Rake.application.invoke_task(task_string)
|
||||
ensure
|
||||
name, _args = Rake.application.parse_task_string(task_string)
|
||||
Rake::Task[name].reenable
|
||||
end
|
||||
end
|
||||
@@ -133,8 +133,8 @@ class Invoice
|
||||
end
|
||||
|
||||
def tax_rate_by_id
|
||||
all_tax_adjustments.each_with_object({}) do |adjustment, tax_rates|
|
||||
tax_rates[adjustment.originator.id] = adjustment.originator
|
||||
all_tax_adjustments.to_h do |adjustment|
|
||||
[adjustment.originator.id, adjustment.originator]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -225,9 +225,17 @@ module Spree
|
||||
OpenFoodNetwork::Permissions.new(user).
|
||||
enterprises_granting_linked_variants.include? variant.supplier
|
||||
end
|
||||
can [
|
||||
:admin,
|
||||
:index,
|
||||
:bulk_update,
|
||||
:destroy,
|
||||
:destroy_variant,
|
||||
:clone,
|
||||
:create_linked_variant
|
||||
], :products_v3
|
||||
|
||||
can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone,
|
||||
:create_linked_variant], :products_v3
|
||||
can [:admin, :producers, :categories, :tax_categories], :ajax_search
|
||||
|
||||
can [:create], Spree::Variant
|
||||
can [:admin, :index, :read, :edit,
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
module Spree
|
||||
class DefaultTaxZoneValidator < ActiveModel::Validator
|
||||
def validate(record)
|
||||
return unless record.included_in_price
|
||||
|
||||
return if Zone.default_tax
|
||||
|
||||
record.errors.add(:included_in_price, Spree.t("errors.messages.included_price_validation"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Spree
|
||||
class TaxRate < ApplicationRecord
|
||||
class DefaultTaxZoneValidator < ActiveModel::Validator
|
||||
def validate(record)
|
||||
return unless record.included_in_price
|
||||
|
||||
return if Zone.default_tax
|
||||
|
||||
record.errors.add(:included_in_price, Spree.t("errors.messages.included_price_validation"))
|
||||
end
|
||||
end
|
||||
|
||||
acts_as_paranoid
|
||||
include CalculatedAdjustments
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ module Spree
|
||||
taxons
|
||||
.pluck('spree_taxons.id, enterprises.id AS enterprise_id')
|
||||
.each_with_object({}) do |(taxon_id, enterprise_id), collection|
|
||||
collection[enterprise_id.to_i] ||= Set.new
|
||||
collection[enterprise_id.to_i] << taxon_id
|
||||
collection[enterprise_id.to_i] ||= Set.new
|
||||
collection[enterprise_id.to_i] << taxon_id
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module Admin
|
||||
class TagRuleSerializer < ActiveModel::Serializer
|
||||
delegate :serializable_hash, to: :rule_specific_serializer
|
||||
|
||||
def rule_specific_serializer
|
||||
"Api::Admin::#{object.class}Serializer".constantize.new(object)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Api
|
||||
module Admin
|
||||
module TagRule
|
||||
|
||||
@@ -6,7 +6,7 @@ module Orders
|
||||
|
||||
delegate :distributor, :order_cycle, to: :order
|
||||
|
||||
FeeValue = Struct.new(:fee, :role, keyword_init: true)
|
||||
FeeValue = Struct.new(:fee, :role)
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
|
||||
@@ -9,14 +9,22 @@
|
||||
- if producer_options.many?
|
||||
.producers
|
||||
= label_tag :producer_id, t('.producers.label')
|
||||
= select_tag :producer_id, options_for_select(producer_options, producer_id),
|
||||
include_blank: t('.all_producers'), class: "fullwidth",
|
||||
data: { "controller": "tom-select", 'tom-select-placeholder-value': t('.search_for_producers')}
|
||||
= render(SearchableDropdownComponent.new(name: :producer_id,
|
||||
aria_label: t('.producers.label'),
|
||||
options: selected_option(producer_id, Enterprise),
|
||||
selected_option: producer_id,
|
||||
remote_url: admin_ajax_search_producers_url,
|
||||
include_blank: t('.all_producers'),
|
||||
placeholder_value: t('.search_for_producers')))
|
||||
.categories
|
||||
= label_tag :category_id, t('.categories.label')
|
||||
= select_tag :category_id, options_for_select(category_options, category_id),
|
||||
include_blank: t('.all_categories'), class: "fullwidth",
|
||||
data: { "controller": "tom-select", 'tom-select-placeholder-value': t('.search_for_categories')}
|
||||
= render(SearchableDropdownComponent.new(name: :category_id,
|
||||
aria_label: t('.categories.label'),
|
||||
options: selected_option(category_id, Spree::Taxon),
|
||||
selected_option: category_id,
|
||||
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)
|
||||
.tags
|
||||
= label_tag :tags_name_in, t('.tags.label')
|
||||
|
||||
@@ -59,27 +59,28 @@
|
||||
= render(SearchableDropdownComponent.new(form: f,
|
||||
name: :supplier_id,
|
||||
aria_label: t('.producer_field_name'),
|
||||
options: producer_options,
|
||||
options: variant.supplier_id ? [[variant.supplier.name, variant.supplier_id]] : [],
|
||||
selected_option: variant.supplier_id,
|
||||
include_blank: t('admin.products_v3.filters.select_producer'),
|
||||
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
|
||||
= render(SearchableDropdownComponent.new(form: f,
|
||||
name: :primary_taxon_id,
|
||||
options: category_options,
|
||||
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'),
|
||||
include_blank: t('admin.products_v3.filters.select_category'),
|
||||
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
|
||||
= render(SearchableDropdownComponent.new(form: f,
|
||||
name: :tax_category_id,
|
||||
options: tax_category_options,
|
||||
options: variant.tax_category_id ? [[variant.tax_category.name, variant.tax_category_id]] : [],
|
||||
selected_option: variant.tax_category_id,
|
||||
include_blank: t('.none_tax_category'),
|
||||
aria_label: t('.tax_category_field_name'),
|
||||
include_blank: t('.none_tax_category'),
|
||||
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)
|
||||
|
||||
@@ -83,6 +83,9 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
#addRemoteOptions(options) {
|
||||
// by default, for dropdown_input plugin, it's true. Otherwise for multi-select it's false
|
||||
// it should always be true so to invoke the onDropdownOpen to fetch options
|
||||
options.shouldOpen = true;
|
||||
this.openedByClick = false;
|
||||
|
||||
options.firstUrl = (query) => {
|
||||
@@ -91,12 +94,9 @@ export default class extends Controller {
|
||||
|
||||
options.load = this.#fetchOptions.bind(this);
|
||||
|
||||
options.onFocus = function () {
|
||||
this.control.load("", () => {});
|
||||
}.bind(this);
|
||||
|
||||
options.onDropdownOpen = function () {
|
||||
this.openedByClick = true;
|
||||
this.control.load("", () => {});
|
||||
}.bind(this);
|
||||
|
||||
options.onType = function () {
|
||||
|
||||
@@ -7,6 +7,15 @@ redis_connection_settings = {
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = redis_connection_settings
|
||||
config.on(:startup) do
|
||||
# Load schedule file similar to sidekiq/cli.rb loading the main config.
|
||||
path = File.expand_path("../sidekiq_scheduler.yml", __dir__)
|
||||
erb = ERB.new(File.read(path), trim_mode: "-")
|
||||
|
||||
Sidekiq.schedule =
|
||||
YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true)
|
||||
SidekiqScheduler::Scheduler.instance.reload_schedule!
|
||||
end
|
||||
end
|
||||
|
||||
Sidekiq.configure_client do |config|
|
||||
|
||||
@@ -83,6 +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'
|
||||
|
||||
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
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Force manual loading of rails application to get all env variables from dotenv-rails when running whenever cmd
|
||||
require File.expand_path('../environment', __FILE__)
|
||||
|
||||
require 'whenever'
|
||||
require 'yaml'
|
||||
|
||||
# Learn more: http://github.com/javan/whenever
|
||||
|
||||
env "MAILTO", ENV["SCHEDULE_NOTIFICATIONS"] if ENV["SCHEDULE_NOTIFICATIONS"]
|
||||
|
||||
# If we use -e with a file containing specs, rspec interprets it and filters out our examples
|
||||
job_type :run_file, "cd :path; :environment_variable=:environment bundle exec script/rails runner :task :output"
|
||||
|
||||
every 1.month, at: '4:30am' do
|
||||
rake 'ofn:data:remove_transient_data'
|
||||
end
|
||||
|
||||
every 1.day, at: '2:45am' do
|
||||
rake 'db2fog:clean' if ENV['S3_BACKUPS_BUCKET']
|
||||
end
|
||||
|
||||
every 4.hours do
|
||||
rake 'db2fog:backup' if ENV['S3_BACKUPS_BUCKET']
|
||||
end
|
||||
@@ -7,15 +7,8 @@
|
||||
- default
|
||||
- mailers
|
||||
|
||||
:scheduler:
|
||||
:schedule:
|
||||
HeartbeatJob:
|
||||
every: ["5m", first_in: "0s"]
|
||||
SubscriptionPlacementJob:
|
||||
every: "5m"
|
||||
SubscriptionConfirmJob:
|
||||
every: "5m"
|
||||
TriggerOrderCyclesToOpenJob:
|
||||
every: "5m"
|
||||
OrderCycleClosingJob:
|
||||
every: "5m"
|
||||
# This config is loaded by sidekiq before dotenv is loading our server config.
|
||||
# Therefore we load the schedule later. See:
|
||||
#
|
||||
# - config/initializers/sidekiq.rb
|
||||
# - config/sidekiq_scheduler.yml
|
||||
|
||||
36
config/sidekiq_scheduler.yml
Normal file
36
config/sidekiq_scheduler.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
# Configure sidekiq-scheduler to run jobs.
|
||||
#
|
||||
# - https://github.com/sidekiq-scheduler/sidekiq-scheduler
|
||||
#
|
||||
# > Note that every and interval count from when the Sidekiq process (re)starts.
|
||||
# > So every: '48h' will never run if the Sidekiq process is restarted daily,
|
||||
# > for example. You can do every: ['48h', first_in: '0s'] to make the job run
|
||||
# > immediately after a restart, and then have the worker check when it was
|
||||
# > last run.
|
||||
#
|
||||
# Therefore, we use `cron` for jobs that should run at certain times like backups.
|
||||
HeartbeatJob:
|
||||
every: ["5m", first_in: "0s"]
|
||||
SubscriptionPlacementJob:
|
||||
every: "5m"
|
||||
SubscriptionConfirmJob:
|
||||
every: "5m"
|
||||
TriggerOrderCyclesToOpenJob:
|
||||
every: "5m"
|
||||
OrderCycleClosingJob:
|
||||
every: "5m"
|
||||
|
||||
backup:
|
||||
class: "RakeJob"
|
||||
args: ["db2fog:backup"]
|
||||
cron: "0 */4 * * *" # every 4 hours
|
||||
enabled: <%= ENV.fetch("S3_BACKUPS_BUCKET", false) && true %>
|
||||
backup_clean:
|
||||
class: "RakeJob"
|
||||
args: ["db2fog:clean"]
|
||||
cron: "45 2 * * *" # every day at 2:45am
|
||||
enabled: <%= ENV.fetch("S3_BACKUPS_BUCKET", false) && true %>
|
||||
ofn_clean:
|
||||
class: "RakeJob"
|
||||
args: ["ofn:data:remove_transient_data"]
|
||||
cron: "30 4 1 * *" # every month on the first at 4:30am
|
||||
@@ -41,8 +41,8 @@ module OpenFoodNetwork
|
||||
end
|
||||
|
||||
def fees_name_by_type_for(variant)
|
||||
per_item_enterprise_fee_applicators_for(variant).each_with_object({}) do |applicator, fees|
|
||||
fees[applicator.enterprise_fee.fee_type.to_sym] = applicator.enterprise_fee.name
|
||||
per_item_enterprise_fee_applicators_for(variant).to_h do |applicator|
|
||||
[applicator.enterprise_fee.fee_type.to_sym, applicator.enterprise_fee.name]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -10,6 +10,4 @@ module OpenFoodNetwork::Locking
|
||||
end
|
||||
end
|
||||
|
||||
class ActiveRecord::Base
|
||||
extend OpenFoodNetwork::Locking
|
||||
end
|
||||
ActiveSupport.on_load(:active_record) { extend OpenFoodNetwork::Locking }
|
||||
|
||||
@@ -1,52 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spree/responder'
|
||||
|
||||
module ActionController
|
||||
class Base
|
||||
def respond_with(*resources, &)
|
||||
if self.class.mimes_for_respond_to.empty?
|
||||
raise "In order to use respond_with, first you need to declare the formats your " \
|
||||
"controller responds to in the class level"
|
||||
end
|
||||
|
||||
return unless (collector = retrieve_collector_from_mimes(&))
|
||||
|
||||
options = resources.size == 1 ? {} : resources.extract_options!
|
||||
|
||||
# Fix spree issues #3531 and #2210 (patch provided by leiyangyou)
|
||||
if (defined_response = collector.response) &&
|
||||
!ApplicationController.spree_responders[self.class.to_s.to_sym].try(:[],
|
||||
action_name.to_sym)
|
||||
if action = options.delete(:action)
|
||||
render(action:)
|
||||
else
|
||||
defined_response.call
|
||||
end
|
||||
else
|
||||
# The action name is needed for processing
|
||||
options[:action_name] = action_name.to_sym
|
||||
# If responder is not specified then pass in Spree::Responder
|
||||
(options.delete(:responder) || Spree::Responder).call(self, resources, options)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def retrieve_collector_from_mimes(mimes = nil, &block)
|
||||
mimes ||= collect_mimes_from_class_level
|
||||
collector = Collector.new(mimes, request.variant)
|
||||
block.call(collector) if block_given?
|
||||
format = collector.negotiate_format(request)
|
||||
|
||||
raise ActionController::UnknownFormat unless format
|
||||
|
||||
_process_format(format)
|
||||
collector
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Spree
|
||||
module Core
|
||||
module ControllerHelpers
|
||||
|
||||
@@ -166,7 +166,6 @@ describe("TomSelectController", () => {
|
||||
expect(settings.searchField).toBe("label");
|
||||
expect(settings.load).toEqual(expect.any(Function));
|
||||
expect(settings.firstUrl).toEqual(expect.any(Function));
|
||||
expect(settings.onFocus).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it("fetches page 1 on focus", async () => {
|
||||
|
||||
18
spec/jobs/rake_job_spec.rb
Normal file
18
spec/jobs/rake_job_spec.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "tasks/data/remove_transient_data"
|
||||
|
||||
RSpec.describe RakeJob do
|
||||
let(:task_string) { "ofn:data:remove_transient_data" }
|
||||
|
||||
it "calls the removal service" do
|
||||
expect(RemoveTransientData).to receive(:new).and_call_original
|
||||
RakeJob.perform_now(task_string)
|
||||
end
|
||||
|
||||
it "can be called several times" do
|
||||
expect(RemoveTransientData).to receive(:new).twice.and_call_original
|
||||
RakeJob.perform_now(task_string)
|
||||
RakeJob.perform_now(task_string)
|
||||
end
|
||||
end
|
||||
285
spec/requests/admin/ajax_search_spec.rb
Normal file
285
spec/requests/admin/ajax_search_spec.rb
Normal file
@@ -0,0 +1,285 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "/admin/ajax_search" 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
|
||||
@@ -145,7 +145,7 @@ RSpec.describe "Customers", swagger_doc: "v1.yaml", feature: :api_v1 do
|
||||
it "adds balance to each customer" do
|
||||
get "/api/v1/customers", params: { extra_fields: { customer: :balance } }
|
||||
balances = json_response[:data].map{ |c| c[:attributes][:balance] }
|
||||
expect(balances.all?{ |b| b.is_a? Numeric }).to eq(true)
|
||||
expect(balances.all?(Numeric)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -19,12 +19,25 @@ module TomSelectHelper
|
||||
tomselect_wrapper.find(:css, '.ts-dropdown div.create').click
|
||||
end
|
||||
|
||||
# Searches for and selects an option in a TomSelect dropdown with search functionality.
|
||||
# @param value [String] The text to search for and select from the dropdown
|
||||
# @param options [Hash] Configuration options
|
||||
# @option options [String] :from The name/id of the select field
|
||||
# @option options [Boolean] :remote_search If true, waits for search loading after interactions
|
||||
#
|
||||
# @example
|
||||
# tomselect_search_and_select("Apple", from: "fruit_selector")
|
||||
# tomselect_search_and_select("California", from: "state", remote_search: true)
|
||||
def tomselect_search_and_select(value, options)
|
||||
tomselect_wrapper = page.find_field(options[:from]).sibling(".ts-wrapper")
|
||||
tomselect_wrapper.find(".ts-control").click
|
||||
expect_tomselect_loading_completion(tomselect_wrapper, options)
|
||||
|
||||
# Use send_keys as setting the value directly doesn't trigger the search
|
||||
tomselect_wrapper.find(".ts-dropdown input.dropdown-input").send_keys(value)
|
||||
tomselect_wrapper.find(".ts-dropdown .ts-dropdown-content .option.active", text: value).click
|
||||
expect_tomselect_loading_completion(tomselect_wrapper, options)
|
||||
|
||||
tomselect_wrapper.find(".ts-dropdown .ts-dropdown-content .option", text: value).click
|
||||
end
|
||||
|
||||
def tomselect_select(value, options)
|
||||
@@ -64,4 +77,52 @@ module TomSelectHelper
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Validates both available options and selected options in a TomSelect dropdown.
|
||||
# @param from [String] The name/id of the select field
|
||||
# @param existing_options [Array<String>] List of options that should be available in the dropdown
|
||||
# @param selected_options [Array<String>] List of options that should currently be selected
|
||||
#
|
||||
# @example
|
||||
# expect_tomselect_existing_with_selected_options(
|
||||
# from: "category_selector",
|
||||
# existing_options: ["Fruit", "Vegetables", "Dairy"],
|
||||
# selected_options: ["Fruit"]
|
||||
# )
|
||||
def expect_tomselect_existing_with_selected_options(from:, existing_options:, selected_options:)
|
||||
tomselect_wrapper = page.find_field(from).sibling(".ts-wrapper")
|
||||
tomselect_control = tomselect_wrapper.find('.ts-control')
|
||||
|
||||
tomselect_control.click # open the dropdown (would work for remote vs non-remote dropdowns)
|
||||
|
||||
# validate existing options are present in the dropdown
|
||||
within(tomselect_wrapper) do
|
||||
existing_options.each do |option|
|
||||
expect(page).to have_css(
|
||||
".ts-dropdown .ts-dropdown-content .option",
|
||||
text: option
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# validate selected options are selected in the dropdown
|
||||
within(tomselect_wrapper) do
|
||||
selected_options.each do |option|
|
||||
expect(page).to have_css(
|
||||
"div[data-ts-item]",
|
||||
text: option
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# close the dropdown by clicking on the already selected option
|
||||
tomselect_wrapper.find(".ts-dropdown .ts-dropdown-content .option.active").click
|
||||
end
|
||||
|
||||
def expect_tomselect_loading_completion(tomselect_wrapper, options)
|
||||
return unless options[:remote_search]
|
||||
|
||||
expect(tomselect_wrapper).to have_css(".spinner")
|
||||
expect(tomselect_wrapper).not_to have_css(".spinner")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,10 +15,6 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
|
||||
login_as user
|
||||
end
|
||||
|
||||
let(:producer_search_selector) { 'input[placeholder="Select producer"]' }
|
||||
let(:categories_search_selector) { 'input[placeholder="Select category"]' }
|
||||
let(:tax_categories_search_selector) { 'input[placeholder="Search for tax categories"]' }
|
||||
|
||||
describe "column selector" do
|
||||
let!(:product) { create(:simple_product) }
|
||||
|
||||
@@ -102,54 +98,7 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
|
||||
}
|
||||
let!(:product_a) { create(:simple_product, name: "Apples", sku: "APL-00") }
|
||||
|
||||
context "when they are under 11" do
|
||||
before do
|
||||
create_list(:supplier_enterprise, 9, users: [user])
|
||||
create_list(:tax_category, 9)
|
||||
create_list(:taxon, 2)
|
||||
|
||||
visit admin_products_url
|
||||
end
|
||||
|
||||
it "should not display search input, change the producers, category and tax category" do
|
||||
producer_to_select = random_producer(variant_a1)
|
||||
category_to_select = random_category(variant_a1)
|
||||
tax_category_to_select = random_tax_category
|
||||
|
||||
within row_containing_name(variant_a1.display_name) do
|
||||
validate_tomselect_without_search!(
|
||||
page, "Producer",
|
||||
producer_search_selector
|
||||
)
|
||||
tomselect_select(producer_to_select, from: "Producer")
|
||||
end
|
||||
|
||||
within row_containing_name(variant_a1.display_name) do
|
||||
validate_tomselect_without_search!(
|
||||
page, "Category",
|
||||
categories_search_selector
|
||||
)
|
||||
tomselect_select(category_to_select, from: "Category")
|
||||
|
||||
validate_tomselect_without_search!(
|
||||
page, "Tax Category",
|
||||
tax_categories_search_selector
|
||||
)
|
||||
tomselect_select(tax_category_to_select, from: "Tax Category")
|
||||
end
|
||||
|
||||
click_button "Save changes"
|
||||
|
||||
expect(page).to have_content "Changes saved"
|
||||
|
||||
variant_a1.reload
|
||||
expect(variant_a1.supplier.name).to eq(producer_to_select)
|
||||
expect(variant_a1.primary_taxon.name).to eq(category_to_select)
|
||||
expect(variant_a1.tax_category.name).to eq(tax_category_to_select)
|
||||
end
|
||||
end
|
||||
|
||||
context "when they are over 11" do
|
||||
context "when there are products" do
|
||||
before do
|
||||
create_list(:supplier_enterprise, 11, users: [user])
|
||||
create_list(:tax_category, 11)
|
||||
@@ -167,9 +116,13 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
|
||||
tax_category_to_select = random_tax_category
|
||||
|
||||
within row_containing_name(variant_a1.display_name) do
|
||||
tomselect_search_and_select(producer_to_select, from: "Producer")
|
||||
tomselect_search_and_select(category_to_select, from: "Category")
|
||||
tomselect_search_and_select(tax_category_to_select, from: "Tax Category")
|
||||
tomselect_search_and_select(producer_to_select, from: "Producer", remote_search: true)
|
||||
tomselect_search_and_select(category_to_select, from: "Category", remote_search: true)
|
||||
tomselect_search_and_select(
|
||||
tax_category_to_select,
|
||||
from: "Tax Category",
|
||||
remote_search: true
|
||||
)
|
||||
end
|
||||
|
||||
click_button "Save changes"
|
||||
|
||||
@@ -86,14 +86,14 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
find('button[aria-label="On Hand"]').click
|
||||
find('input[id$="_price"]').fill_in with: "11.1"
|
||||
|
||||
select supplier.name, from: 'Producer'
|
||||
select taxon.name, from: 'Category'
|
||||
|
||||
if stock == "on_hand"
|
||||
find('input[id$="_on_hand_desired"]').fill_in with: "66"
|
||||
elsif stock == "on_demand"
|
||||
find('input[id$="_on_demand_desired"]').check
|
||||
end
|
||||
|
||||
tomselect_select supplier.name, from: 'Producer'
|
||||
tomselect_select taxon.name, from: 'Category'
|
||||
end
|
||||
|
||||
expect(page).to have_content "1 product modified."
|
||||
|
||||
@@ -106,13 +106,19 @@ RSpec.describe 'As an enterprise user, I can browse my products' do
|
||||
visit spree.admin_products_path
|
||||
|
||||
within row_containing_name "Variant1" do
|
||||
expect(page).to have_select "Producer", with_options: ["Producer A", "Producer B"],
|
||||
selected: "Producer A"
|
||||
expect_tomselect_existing_with_selected_options(
|
||||
from: 'Producer',
|
||||
existing_options: ["Producer A", "Producer B"],
|
||||
selected_options: ["Producer A"]
|
||||
)
|
||||
end
|
||||
|
||||
within row_containing_name "Variant2a" do
|
||||
expect(page).to have_select "Producer", with_options: ["Producer A", "Producer B"],
|
||||
selected: "Producer B"
|
||||
expect_tomselect_existing_with_selected_options(
|
||||
from: 'Producer',
|
||||
existing_options: ["Producer A", "Producer B"],
|
||||
selected_options: ["Producer B"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -543,24 +549,21 @@ RSpec.describe 'As an enterprise user, I can browse my products' do
|
||||
|
||||
it "shows only suppliers that I manage or have permission to" do
|
||||
visit spree.admin_products_path
|
||||
existing_options = [supplier_managed1.name, supplier_managed2.name, supplier_permitted.name]
|
||||
|
||||
within row_containing_placeholder(product_supplied.name) do
|
||||
expect(page).to have_select(
|
||||
'_products_0_variants_attributes_0_supplier_id',
|
||||
options: [
|
||||
'Select producer',
|
||||
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
|
||||
], selected: supplier_managed1.name
|
||||
expect_tomselect_existing_with_selected_options(
|
||||
existing_options:,
|
||||
from: '_products_0_variants_attributes_0_supplier_id',
|
||||
selected_options: [supplier_managed1.name]
|
||||
)
|
||||
end
|
||||
|
||||
within row_containing_placeholder(product_supplied_permitted.name) do
|
||||
expect(page).to have_select(
|
||||
'_products_1_variants_attributes_0_supplier_id',
|
||||
options: [
|
||||
'Select producer',
|
||||
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
|
||||
], selected: supplier_permitted.name
|
||||
expect_tomselect_existing_with_selected_options(
|
||||
existing_options:,
|
||||
from: '_products_1_variants_attributes_0_supplier_id',
|
||||
selected_options: [supplier_permitted.name]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -350,8 +350,8 @@ RSpec.describe 'As an enterprise user, I can update my products' do
|
||||
click_on "On Hand" # activate popout
|
||||
fill_in "On Hand", with: "3"
|
||||
|
||||
select producer.name, from: 'Producer'
|
||||
select taxon.name, from: 'Category'
|
||||
tomselect_select producer.name, from: 'Producer'
|
||||
tomselect_select taxon.name, from: 'Category'
|
||||
end
|
||||
|
||||
expect {
|
||||
@@ -586,8 +586,8 @@ RSpec.describe 'As an enterprise user, I can update my products' do
|
||||
fill_in "Name", with: "Nice box"
|
||||
fill_in "SKU", with: "APL-02"
|
||||
|
||||
select producer.name, from: 'Producer'
|
||||
select taxon.name, from: 'Category'
|
||||
tomselect_select producer.name, from: 'Producer'
|
||||
tomselect_select taxon.name, from: 'Category'
|
||||
end
|
||||
|
||||
expect {
|
||||
|
||||
@@ -17,7 +17,7 @@ RSpec.describe "admin/products_v3/_filters.html.haml" do
|
||||
end
|
||||
let(:spree_current_user) { build(:enterprise_user) }
|
||||
|
||||
it "shows the producer filter when there are options" do
|
||||
it "shows the producer filter with the default option initially" do
|
||||
allow(view).to receive_messages locals.merge(
|
||||
producer_options: [
|
||||
["Ada's Apples", 1],
|
||||
@@ -27,9 +27,7 @@ RSpec.describe "admin/products_v3/_filters.html.haml" do
|
||||
|
||||
is_expected.to have_content "Producers"
|
||||
is_expected.to have_select "producer_id", options: [
|
||||
"All producers",
|
||||
"Ada's Apples",
|
||||
"Ben's Bananas",
|
||||
"All producers"
|
||||
], selected: nil
|
||||
end
|
||||
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -5270,9 +5270,9 @@ node-addon-api@^7.0.0:
|
||||
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
|
||||
|
||||
node-forge@^1:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
|
||||
integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2"
|
||||
integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==
|
||||
|
||||
node-int64@^0.4.0:
|
||||
version "0.4.0"
|
||||
@@ -7111,9 +7111,9 @@ tr46@^5.1.0:
|
||||
punycode "^2.3.1"
|
||||
|
||||
trix@*:
|
||||
version "2.1.17"
|
||||
resolved "https://registry.yarnpkg.com/trix/-/trix-2.1.17.tgz#a47c4ee1925a7abb26aee5c094ec7ef9fe49575a"
|
||||
integrity sha512-nkHg7VgIItGVx1CFA645dDlAoCgah+9gv80Yc+97aS8jkZmO5K4MxkSqmU9t/C8upqZB8uhfJs90epoDfYz/6Q==
|
||||
version "2.1.18"
|
||||
resolved "https://registry.yarnpkg.com/trix/-/trix-2.1.18.tgz#d87a5be64c10ecdab64f5af47f941b5318c08225"
|
||||
integrity sha512-DWOdTsz3n9PO3YBc1R6pGh9MG1cXys/2+rouc/qsISncjc2MBew2UOW8nXh3NjUOjobKsXCIPR6LB02abg2EYg==
|
||||
dependencies:
|
||||
dompurify "^3.2.5"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user