From 3d957d31791da07a0ecbc7feac467499623a60d1 Mon Sep 17 00:00:00 2001 From: Tomasz Donarski Date: Mon, 13 Nov 2023 08:46:49 +0100 Subject: [PATCH] Initial implementation of bulk promo codes admin UI --- .../admin/promotion_batches_controller.rb | 63 +++++++++++++++++ .../spree/admin/promotions_controller.rb | 1 + app/helpers/spree/admin/navigation_helper.rb | 8 +++ ...promotion_batch_default_actions_builder.rb | 27 ++++++++ ...omotion_batches_default_actions_builder.rb | 28 ++++++++ .../default_configuration_builder.rb | 8 +++ .../admin/promotion_batches/_form.html.erb | 6 ++ .../admin/promotion_batches/_size.html.erb | 6 ++ .../admin/promotion_batches/edit.html.erb | 68 +++++++++++++++++++ .../admin/promotion_batches/index.html.erb | 45 ++++++++++++ .../admin/promotion_batches/new.html.erb | 13 ++++ .../admin/promotion_batches/show.html.erb | 37 ++++++++++ config/routes.rb | 8 +++ lib/spree/backend/engine.rb | 4 ++ ...tion_batch_default_actions_builder_spec.rb | 20 ++++++ ...on_batches_default_actions_builder_spec.rb | 20 ++++++ .../default_configuration_builder_spec.rb | 2 +- 17 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 app/controllers/spree/admin/promotion_batches_controller.rb create mode 100644 app/models/spree/admin/actions/promotion_batch_default_actions_builder.rb create mode 100644 app/models/spree/admin/actions/promotion_batches_default_actions_builder.rb create mode 100644 app/views/spree/admin/promotion_batches/_form.html.erb create mode 100644 app/views/spree/admin/promotion_batches/_size.html.erb create mode 100644 app/views/spree/admin/promotion_batches/edit.html.erb create mode 100644 app/views/spree/admin/promotion_batches/index.html.erb create mode 100644 app/views/spree/admin/promotion_batches/new.html.erb create mode 100644 app/views/spree/admin/promotion_batches/show.html.erb create mode 100644 spec/models/spree/admin/actions/promotion_batch_default_actions_builder_spec.rb create mode 100644 spec/models/spree/admin/actions/promotion_batches_default_actions_builder_spec.rb diff --git a/app/controllers/spree/admin/promotion_batches_controller.rb b/app/controllers/spree/admin/promotion_batches_controller.rb new file mode 100644 index 0000000000..d60e3e83b5 --- /dev/null +++ b/app/controllers/spree/admin/promotion_batches_controller.rb @@ -0,0 +1,63 @@ +module Spree + module Admin + class PromotionBatchesController < ResourceController + def update + if @object.template_promotion_id + flash[:error] = Spree.t(:template_promotion_already_assigned) + respond_with(@object) do |format| + format.html { render action: :edit, status: :unprocessable_entity } + format.js { render layout: false, status: :unprocessable_entity } + end + return + end + super + end + + def destroy + result = Spree::PromotionBatches::Destroy.call(promotion_batch: @promotion_batch) + + if result.success? + flash[:success] = flash_message_for(@promotion_batch, :successfully_removed) + else + flash[:error] = @promotion_batch.errors.full_messages.join(', ') + end + + respond_with(@promotion_batch) do |format| + format.html { redirect_to location_after_destroy } + format.js { render_js_for_destroy } + end + end + + def csv_export + send_data Spree::PromotionBatches::PromotionCodesExporter.new(params).call, + filename: "promo_codes_from_batch_id_#{params[:id]}.csv", + disposition: :attachment, + type: 'text/csv' + end + + def csv_import + file = params[:file] + Spree::PromotionBatches::PromotionCodesImporter.new(file: file, promotion_batch_id: params[:id]).call + redirect_back fallback_location: admin_promotions_path, notice: Spree.t('code_upload') + rescue Spree::PromotionBatches::PromotionCodesImporter::Error => e + redirect_back fallback_location: admin_promotions_path, alert: e.message + end + + def populate + batch_id = params[:id] + options = { + batch_size: params[:batch_size].to_i, + affix: params.dig(:code, :affix)&.to_sym, + content: params[:affix_content], + deny_list: params[:forbidden_phrases].split, + random_part_bytes: params[:random_part_bytes].to_i + } + + Spree::Promotions::PopulatePromotionBatch.new(batch_id, options).call + + flash[:success] = Spree.t('promotion_batch_populated') + redirect_to spree.edit_admin_promotion_batch_url(@promotion_batch) + end + end + end +end diff --git a/app/controllers/spree/admin/promotions_controller.rb b/app/controllers/spree/admin/promotions_controller.rb index 23a616cc90..4b66698654 100644 --- a/app/controllers/spree/admin/promotions_controller.rb +++ b/app/controllers/spree/admin/promotions_controller.rb @@ -40,6 +40,7 @@ def collection params[:q][:s] ||= 'id desc' @collection = super + @collection = @collection.non_batched @search = @collection.ransack(params[:q]) @collection = @search.result(distinct: true). includes(promotion_includes). diff --git a/app/helpers/spree/admin/navigation_helper.rb b/app/helpers/spree/admin/navigation_helper.rb index dfecab836c..7b6c81a7b8 100644 --- a/app/helpers/spree/admin/navigation_helper.rb +++ b/app/helpers/spree/admin/navigation_helper.rb @@ -377,6 +377,14 @@ def variants_actions def product_properties_actions Rails.application.config.spree_backend.actions[:product_properties] end + + def promotion_batch_actions + Rails.application.config.spree_backend.actions[:promotion_batch_actions] + end + + def promotion_batches_actions + Rails.application.config.spree_backend.actions[:promotion_batches_actions] + end # rubocop:enable Metrics/ModuleLength end end diff --git a/app/models/spree/admin/actions/promotion_batch_default_actions_builder.rb b/app/models/spree/admin/actions/promotion_batch_default_actions_builder.rb new file mode 100644 index 0000000000..f9c7979b9d --- /dev/null +++ b/app/models/spree/admin/actions/promotion_batch_default_actions_builder.rb @@ -0,0 +1,27 @@ +module Spree + module Admin + module Actions + class PromotionBatchDefaultActionsBuilder + include Spree::Core::Engine.routes.url_helpers + + def build + root = Root.new + export_codes_to_csv_action(root) + root + end + + private + + def export_codes_to_csv_action(root) + action = + ActionBuilder.new('csv_export', ->(resource) { csv_export_admin_promotion_batch_path(resource) }). + with_icon_key('download.svg'). + with_style(::Spree::Admin::Actions::ActionStyle::PRIMARY). + build + + root.add(action) + end + end + end + end +end diff --git a/app/models/spree/admin/actions/promotion_batches_default_actions_builder.rb b/app/models/spree/admin/actions/promotion_batches_default_actions_builder.rb new file mode 100644 index 0000000000..79a6ea955f --- /dev/null +++ b/app/models/spree/admin/actions/promotion_batches_default_actions_builder.rb @@ -0,0 +1,28 @@ +module Spree + module Admin + module Actions + class PromotionBatchesDefaultActionsBuilder + include Spree::Core::Engine.routes.url_helpers + + def build + root = Root.new + add_new_promotion_batch_action(root) + root + end + + private + + def add_new_promotion_batch_action(root) + action = + ActionBuilder.new('new_promotion_batch', new_admin_promotion_batch_path). + with_icon_key('add.svg'). + with_style(::Spree::Admin::Actions::ActionStyle::PRIMARY). + with_create_ability_check(::Spree::PromotionBatch). + build + + root.add(action) + end + end + end + end +end diff --git a/app/models/spree/admin/main_menu/default_configuration_builder.rb b/app/models/spree/admin/main_menu/default_configuration_builder.rb index 4ad4887506..b524eaada3 100644 --- a/app/models/spree/admin/main_menu/default_configuration_builder.rb +++ b/app/models/spree/admin/main_menu/default_configuration_builder.rb @@ -19,6 +19,7 @@ def build add_integrations_section(root) add_oauth_section(root) add_settings_section(root) + add_promotion_batches_section(root) root end @@ -255,6 +256,13 @@ def add_settings_section(root) build root.add(section) end + + def add_promotion_batches_section(root) + root.add(ItemBuilder.new('promotion_batches', admin_promotion_batches_path). + with_icon_key('stack.svg'). + with_admin_ability_check(Spree::PromotionBatch). + build) + end # rubocop:enable Metrics/AbcSize end # rubocop:enable Metrics/ClassLength diff --git a/app/views/spree/admin/promotion_batches/_form.html.erb b/app/views/spree/admin/promotion_batches/_form.html.erb new file mode 100644 index 0000000000..c831fea953 --- /dev/null +++ b/app/views/spree/admin/promotion_batches/_form.html.erb @@ -0,0 +1,6 @@ +
+ <%= f.field_container :template_promotion do %> + <%= f.label :template_promotion_id, Spree.t(:template_promotion) %> + <%= f.collection_select :template_promotion_id, Spree::Promotion.non_batched, :id, ->(promotion) { "#{promotion.name} # #{promotion.id}" }, { include_blank: true }, { class: 'select2-clear w-100' } %> + <% end %> +
diff --git a/app/views/spree/admin/promotion_batches/_size.html.erb b/app/views/spree/admin/promotion_batches/_size.html.erb new file mode 100644 index 0000000000..ac444014fb --- /dev/null +++ b/app/views/spree/admin/promotion_batches/_size.html.erb @@ -0,0 +1,6 @@ +
+ <%= form_tag(duplicate_admin_promotion_batch_path(@promotion_batch), method: :post) do %> + <%= number_field_tag(:batch_size) %> + <%= submit_tag("Duplicate") %> + <% end %> +
\ No newline at end of file diff --git a/app/views/spree/admin/promotion_batches/edit.html.erb b/app/views/spree/admin/promotion_batches/edit.html.erb new file mode 100644 index 0000000000..8c1f21c7f7 --- /dev/null +++ b/app/views/spree/admin/promotion_batches/edit.html.erb @@ -0,0 +1,68 @@ +<% content_for :page_title do %> + <%= link_to Spree.t(:promotion_batches), admin_promotion_batches_path %> / + <%= @promotion_batch.id %> +<% end %> + +<% if @promotion_batch.template_promotion_id%> +
+
+ <%= form_tag csv_import_admin_promotion_batch_path, method: :post, multipart: true do %> + <%= file_field_tag :file, accept: ".csv" %> + <%= submit_tag Spree.t('promotion_batch_form.import_codes') %> + <% end %> +
+
+<% end %> + +<% unless @promotion_batch.template_promotion_id%> + <%= form_for @promotion_batch, url: object_url, method: :put do |f| %> +
+
+ <%= render partial: 'form', locals: { f: f } %> + <%= render partial: 'spree/admin/shared/edit_resource_links' %> +
+
+ <% end %> +<% end %> + +<% if @promotion_batch.template_promotion_id %> + <%= form_with url: populate_admin_promotion_batch_path(@promotion_batch), method: :post do %> +
+ <%= label_tag :batch_size do %> + <%= number_field_tag(:batch_size) %> + <%= Spree.t('promotion_batch_form.batch_size') %> + <% end %> +
+
+ <%= label_tag :prefix do %> + <%= radio_button :code, :affix, 'prefix' %> + <%= Spree.t('promotion_batch_form.prefix') %> + <% end %> +
+
+ <%= label_tag :suffix do %> + <%= radio_button :code, :affix, 'suffix' %> + <%= Spree.t('promotion_batch_form.suffix') %> + <% end %> +
+
+ <%= label_tag :affix_content do %> + <%= text_field_tag(:affix_content) %> + <%= Spree.t('promotion_batch_form.affix_content') %> + <% end %> +
+
+ <%= label_tag :forbidden_phrases do %> + <%= text_area_tag(:forbidden_phrases) %> + <%= Spree.t('promotion_batch_form.forbidden_phrases') %> + <% end %> +
+
+ <%= label_tag :random_part_bytes do %> + <%= number_field_tag(:random_part_bytes, value = 4) %> + <%= Spree.t('promotion_batch_form.random_part_bytes') %> + <% end %> +
+ <%= submit_tag(Spree.t('promotion_batch_form.populate')) %> + <% end %> +<% end %> diff --git a/app/views/spree/admin/promotion_batches/index.html.erb b/app/views/spree/admin/promotion_batches/index.html.erb new file mode 100644 index 0000000000..60b460ae49 --- /dev/null +++ b/app/views/spree/admin/promotion_batches/index.html.erb @@ -0,0 +1,45 @@ +<% content_for :page_title do %> + <%= plural_resource_name(Spree::PromotionBatch) %> +<% end %> + +<% content_for :page_actions do %> + <% promotion_batches_actions.items.each do |action| %> + <% next unless action.available?(current_ability) %> + <%= button_link_to( + Spree.t(action.label_translation_key), + action.url, + class: action.classes, + icon: action.icon_key + ) %> + <% end %> +<% end %> + +<% if @promotion_batches.any? %> +
+ + + + + + + + + + + <% @promotion_batches.each do |promotion_batch| %> + + + + + + + <% end %> + +
<%= Spree.t(:id) %><%= Spree.t(:size) %><%= Spree.t(:template_promotion) %>
<%= link_to Spree::PromotionBatchPresenter.new(promotion_batch).call[:model_name_id], spree.admin_promotion_batch_path(promotion_batch) %><%= promotion_batch.promotions.count %><%= link_to Spree::PromotionBatchPresenter.new(promotion_batch).call[:template_promotion_name_id], spree.edit_admin_promotion_path(promotion_batch.template_promotion) if promotion_batch.template_promotion %> + + <%= link_to_edit promotion_batch, no_text: true if can?(:edit, promotion_batch) %> + <%= link_to_delete promotion_batch, no_text: true if can?(:delete, promotion_batch) %> + +
+
+<% end %> \ No newline at end of file diff --git a/app/views/spree/admin/promotion_batches/new.html.erb b/app/views/spree/admin/promotion_batches/new.html.erb new file mode 100644 index 0000000000..328fe5e0c0 --- /dev/null +++ b/app/views/spree/admin/promotion_batches/new.html.erb @@ -0,0 +1,13 @@ +<% content_for :page_title do %> + <%= link_to Spree.t(:promotion_batches), admin_promotion_batches_path %> / + <%= Spree.t(:new_promotion_batch) %> +<% end %> + +
+
+ <%= form_for :promotion_batch, url: collection_url do |f| %> + <%= render partial: 'form', locals: { f: f } %> + <%= render partial: 'spree/admin/shared/new_resource_links' %> + <% end %> +
+
diff --git a/app/views/spree/admin/promotion_batches/show.html.erb b/app/views/spree/admin/promotion_batches/show.html.erb new file mode 100644 index 0000000000..2bad5bdcfe --- /dev/null +++ b/app/views/spree/admin/promotion_batches/show.html.erb @@ -0,0 +1,37 @@ +<% content_for :page_title do %> + <%= link_to Spree.t(:promotion_batches), admin_promotion_batches_path %> / + <%= @promotion_batch.id %> +<% end %> + +<% content_for :page_actions do %> + <% promotion_batch_actions.items.each do |action| %> + <% next unless action.available?(current_ability) %> + <%= button_link_to( + Spree.t(action.label_translation_key), + action.url(@promotion_batch), + class: action.classes, + icon: action.icon_key + ) %> + <% end %> +<% end %> + + + + + + + + + + + + <% @promotion_batch.promotions.each do |promotion| %> + + + + + + + <% end %> + +
<%= Spree.t(:code) %><%= Spree.t(:description) %><%= Spree.t(:redeemed) %><%= Spree.t(:expiration) %>
<%= promotion.code %><%= promotion.description %><%= promotion.credits_count == promotion.usage_limit ? Spree.t(:say_yes) : Spree.t(:say_no) %><%= promotion.expires_at.to_date if promotion.expires_at %>
diff --git a/config/routes.rb b/config/routes.rb index e1facaf504..c1ee28aa19 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,6 +10,14 @@ resources :promotion_categories, except: [:show] + resources :promotion_batches do + member do + get :csv_export, to: 'promotion_batches#csv_export' + post :csv_import, to: 'promotion_batches#csv_import' + post :populate + end + end + resources :zones resources :stores, except: %i[index show] do diff --git a/lib/spree/backend/engine.rb b/lib/spree/backend/engine.rb index 49887afbff..119358659a 100644 --- a/lib/spree/backend/engine.rb +++ b/lib/spree/backend/engine.rb @@ -48,6 +48,10 @@ class Engine < ::Rails::Engine Rails.application.config.spree_backend.actions[:payments] = Spree::Admin::Actions::PaymentsDefaultActionsBuilder.new.build Rails.application.config.spree_backend.actions[:variants] = Spree::Admin::Actions::VariantsDefaultActionsBuilder.new.build Rails.application.config.spree_backend.actions[:product_properties] = Spree::Admin::Actions::ProductPropertiesDefaultActionsBuilder.new.build + Rails.application.config.spree_backend.actions[:promotion_batch_actions] = + Spree::Admin::Actions::PromotionBatchDefaultActionsBuilder.new.build + Rails.application.config.spree_backend.actions[:promotion_batches_actions] = + Spree::Admin::Actions::PromotionBatchesDefaultActionsBuilder.new.build end end end diff --git a/spec/models/spree/admin/actions/promotion_batch_default_actions_builder_spec.rb b/spec/models/spree/admin/actions/promotion_batch_default_actions_builder_spec.rb new file mode 100644 index 0000000000..2f45ca3460 --- /dev/null +++ b/spec/models/spree/admin/actions/promotion_batch_default_actions_builder_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +module Spree + module Admin + describe Actions::PromotionBatchDefaultActionsBuilder, type: :model do + let(:builder) { described_class.new } + let(:default_actions) do + %w(csv_export) + end + + describe '#build' do + subject { builder.build } + + it 'builds default tabs' do + expect(subject.items.map(&:key)).to match(default_actions) + end + end + end + end +end diff --git a/spec/models/spree/admin/actions/promotion_batches_default_actions_builder_spec.rb b/spec/models/spree/admin/actions/promotion_batches_default_actions_builder_spec.rb new file mode 100644 index 0000000000..848f284f15 --- /dev/null +++ b/spec/models/spree/admin/actions/promotion_batches_default_actions_builder_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +module Spree + module Admin + describe Actions::PromotionBatchesDefaultActionsBuilder, type: :model do + let(:builder) { described_class.new } + let(:default_actions) do + %w(new_promotion_batch) + end + + describe '#build' do + subject { builder.build } + + it 'builds default tabs' do + expect(subject.items.map(&:key)).to match(default_actions) + end + end + end + end +end diff --git a/spec/models/spree/admin/main_menu/default_configuration_builder_spec.rb b/spec/models/spree/admin/main_menu/default_configuration_builder_spec.rb index 353f05a9dc..455a3fec23 100644 --- a/spec/models/spree/admin/main_menu/default_configuration_builder_spec.rb +++ b/spec/models/spree/admin/main_menu/default_configuration_builder_spec.rb @@ -9,7 +9,7 @@ module Admin subject { builder.build } it 'builds a valid menu' do - expect(subject.items.count).to eq(12) + expect(subject.items.count).to eq(13) expect(subject.items.map(&:key)).to include('dashboard') expect(subject.items.map(&:key)).to include('orders') expect(subject.items.map(&:key)).to include('settings')