diff --git a/app/assets/javascripts/admin/enterprises/controllers/tag_rules_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/tag_rules_controller.js.coffee index 4b007c7744..e8c95558df 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/tag_rules_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/tag_rules_controller.js.coffee @@ -1,7 +1,26 @@ angular.module("admin.enterprises").controller "TagRulesCtrl", ($scope) -> - $scope.groupedTagRules = $scope.Enterprise.tag_rules.reduce (groupedTagRules, rule) -> - key = rule.preferred_customer_tags - groupedTagRules[key] ||= [] - groupedTagRules[key].push rule - groupedTagRules - , {} + $scope.tagGroups = $scope.Enterprise.tag_groups + + updateRuleCounts = -> + index = 0 + for tagGroup in $scope.tagGroups + tagGroup.startIndex = index + index = index + tagGroup.rules.length + + updateRuleCounts() + + $scope.updateTagsRulesFor = (tagGroup) -> + for tagRule in tagGroup.rules + tagRule.preferred_customer_tags = (tag.text for tag in tagGroup.tags).join(",") + + $scope.addNewRuleTo = (tagGroup) -> + tagGroup.rules.push + id: null + preferred_customer_tags: (tag.text for tag in tagGroup.tags).join(",") + type: "TagRule::DiscountOrder" + calculator: + preferred_flat_percent: 0 + updateRuleCounts() + + $scope.addNewTag = -> + $scope.tagGroups.push { tags: [], rules: [] } diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index 6be7e00ffa..4537620512 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.dropdown', 'pasvaz.bindonce', 'ngSanitize'] ) \ No newline at end of file +angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.dropdown', 'pasvaz.bindonce', 'ngSanitize', 'ngTagsInput'] ) \ No newline at end of file diff --git a/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml b/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml index 19b4af427e..bc2ddc5c75 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml @@ -1,26 +1,36 @@ %div %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{rule.id}}_id", - name: "enterprise[tag_rules_attributes][{{rule.id}}][id]", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", ng: { value: "rule.id" } } %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{rule.id}}_calculator_attributes_id", - name: "enterprise[tag_rules_attributes][{{rule.id}}][calculator_attributes][id]", - ng: { value: "rule.calculator.id" } } + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", + value: "TagRule::DiscountOrder" } %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{rule.id}}_calculator_attributes_type", - name: "enterprise[tag_rules_attributes][{{rule.id}}][calculator_attributes][type]", - value: "TagRule::FlatPercentItemTotal" } + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", + ng: { value: "rule.preferred_customer_tags" } } - %span.text-big Apply a discount of + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_type]", + value: "Spree::Calculator::FlatPercentItemTotal" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][id]", + ng: { value: "rule.calculator.id" } } + + %span.text-normal {{ $index + 1 }}. Apply a discount of %span.input-symbol.after - %span.text-big % + %span.text-normal % %input.text-big{ type: "number", - id: "enterprise_tag_rules_attributes_{{rule.id}}_calculator_attributes_preferred_flat_percent", - name: "enterprise[tag_rules_attributes][{{rule.id}}][calculator_attributes][preferred_flat_percent]", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_preferred_flat_percent", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][preferred_flat_percent]", min: 0, max: 100, ng: { model: "rule.calculator.preferred_flat_percent" } } - %span.text-big to order subtotals + %span.text-normal to order subtotals diff --git a/app/assets/stylesheets/admin/offsets.css.scss b/app/assets/stylesheets/admin/offsets.css.scss index 762b7469f6..190ed49243 100644 --- a/app/assets/stylesheets/admin/offsets.css.scss +++ b/app/assets/stylesheets/admin/offsets.css.scss @@ -2,6 +2,10 @@ margin-bottom: 20px; } +.margin-bottom-30 { + margin-bottom: 30px; +} + .margin-bottom-50 { margin-bottom: 50px; } diff --git a/app/assets/stylesheets/admin/tag_rules.css.scss b/app/assets/stylesheets/admin/tag_rules.css.scss index eafc5826f7..cbaed55587 100644 --- a/app/assets/stylesheets/admin/tag_rules.css.scss +++ b/app/assets/stylesheets/admin/tag_rules.css.scss @@ -1,5 +1,13 @@ +.no_tags { + margin-bottom: 40px; + color: #aeaeae; + font-size: 1rem; + font-weight: bold; +} + .customer_tag { border: 1px solid #cee1f4; + margin-bottom: 40px; .header { padding: 8px 10px; @@ -7,7 +15,21 @@ border-bottom: 1px solid #cee1f4; } + .no_rules { + padding: 8px 10px; + margin-bottom: 10px; + color: #aeaeae; + font-size: 1rem; + font-weight: bold; + } + .tag_rule { padding: 8px 10px; + margin-bottom: 10px; + } + + .add_rule { + padding: 8px 10px; + margin-bottom: 10px; } } diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index d692984885..377186b604 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -35,6 +35,8 @@ module Admin def update invoke_callbacks(:update, :before) + tag_rules_attributes = params[object_name].delete :tag_rules_attributes + update_tag_rules(tag_rules_attributes) if tag_rules_attributes.present? if @object.update_attributes(params[object_name]) invoke_callbacks(:update, :after) flash[:success] = flash_message_for(@object, :successfully_updated) @@ -180,6 +182,27 @@ module Admin @taxons = Spree::Taxon.order(:name) end + def update_tag_rules(tag_rules_attributes) + # Due to the combination of trying to use nested attributes and type inheritance + # we cannot apply all attributes to tag rules in one hit because mass assignment + # methods that are specific to each class do not become available until after the + # record is persisted. This problem is compounded by the use of calculators. + @object.transaction do + tag_rules_attributes.select{ |i, attrs| attrs[:type].present? }.each do |i, attrs| + rule = @object.tag_rules.find_by_id(attrs.delete :id) || attrs[:type].constantize.new(enterprise: @object) + create_calculator_for(rule, attrs) if rule.type == "TagRule::DiscountOrder" && rule.calculator.nil? + rule.update_attributes(attrs) + end + end + end + + def create_calculator_for(rule, attrs) + if attrs[:calculator_type].present? && attrs[:calculator_attributes].present? + rule.update_attributes(calculator_type: attrs[:calculator_type]) + attrs[:calculator_attributes].merge!( { id: rule.calculator.id } ) + end + end + def check_can_change_bulk_sells unless spree_current_user.admin? params[:enterprise_set][:collection_attributes].each do |i, enterprise_params| diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 62462ac425..dfc5050a38 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -48,7 +48,7 @@ class Enterprise < ActiveRecord::Base accepts_nested_attributes_for :address accepts_nested_attributes_for :producer_properties, allow_destroy: true, reject_if: lambda { |pp| pp[:property_name].blank? } - accepts_nested_attributes_for :tag_rules, allow_destroy: true + accepts_nested_attributes_for :tag_rules, allow_destroy: true, reject_if: lambda { |tag_rule| tag_rule[:preferred_customer_tags].blank? } has_attached_file :logo, styles: { medium: "300x300>", small: "180x180>", thumb: "100x100>" }, diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index 1a3986e2ea..cf0a105e80 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -7,6 +7,8 @@ class TagRule < ActiveRecord::Base validates :enterprise, presence: true + attr_accessible :enterprise, :enterprise_id, :preferred_customer_tags + def set_context(subject, context) @subject = subject @context = context diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 1d9c37595d..8f70da1a0d 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -3,9 +3,25 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :producer_profile_only, :email, :long_description, :permalink attributes :preferred_shopfront_message, :preferred_shopfront_closed_message, :preferred_shopfront_taxon_order, :preferred_shopfront_order_cycle_order attributes :preferred_product_selection_from_inventory_only - attributes :owner, :users + attributes :owner, :users, :tag_groups has_one :owner, serializer: Api::Admin::UserSerializer has_many :users, serializer: Api::Admin::UserSerializer - has_many :tag_rules, serializer: Api::Admin::TagRuleSerializer + + def tag_groups + tag_groups = [] + object.tag_rules.each do |tag_rule| + tag_group = find_match(tag_groups, tag_rule.preferred_customer_tags.split(",").map{ |t| { text: t } }) + tag_groups << tag_group if tag_group[:rules].empty? + tag_group[:rules] << Api::Admin::TagRuleSerializer.new(tag_rule).serializable_hash + end + tag_groups + end + + def find_match(tag_groups, tags) + tag_groups.each do |tag_group| + return tag_group if tag_group[:tags].length == tags.length && (tag_group[:tags] & tags) == tag_group[:tags] + end + return { tags: tags, rules: [] } + end end diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index d602f7418d..e363078733 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -55,6 +55,6 @@ %legend Shop Preferences = render 'admin/enterprises/form/shop_preferences', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Tag Rules'" } } +%fieldset.alpha.no-border-bottom{ ng: { if: "menu.selected.name=='Tag Rules'" } } %legend Tag Rules = render 'admin/enterprises/form/tag_rules', f: f diff --git a/app/views/admin/enterprises/edit.html.haml b/app/views/admin/enterprises/edit.html.haml index 19ae0f3b44..5d3a623f40 100644 --- a/app/views/admin/enterprises/edit.html.haml +++ b/app/views/admin/enterprises/edit.html.haml @@ -7,6 +7,7 @@ - content_for :page_actions do %li= button_link_to "Back to enterprises list", main_app.admin_enterprises_path, icon: 'icon-arrow-left' + = render 'admin/enterprises/form_data' = render 'admin/enterprises/ng_form', action: 'edit' diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 44b1148058..ecb8b54f2e 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -1,10 +1,21 @@ .row{ ng: { controller: "TagRulesCtrl" } } .eleven.columns.alpha.omega .eleven.columns.alpha.omega - .customer_tag{ ng: { repeat: "(tags, rules) in groupedTagRules" }, bindonce: true } + .no_tags{ ng: { show: "tagGroups.length == 0" } } + No tags apply to this enterprise yet + .customer_tag{ ng: { repeat: "tagGroup in tagGroups" }, bindonce: true } .header %h3 - For customers tagged - %span.text-red #{{ tags.split(",").join(", #") }} - .tag_rule{ ng: { repeat: "rule in rules" } } + For customers tagged: + %tags-input{ ng: { model: 'tagGroup.tags'}, + min: { tags: "1" }, + on: { tag: { added: "updateTagsRulesFor(tagGroup)", removed: "updateTagsRulesFor(tagGroup)" } } } + + .no_rules{ ng: { show: "tagGroup.rules.length == 0" } } + No rules apply to this tag yet + .tag_rule{ ng: { repeat: "rule in tagGroup.rules" } } %discount-order{ bo: { if: "rule.type == 'TagRule::DiscountOrder'" } } + .add_rule + %input.button.icon-plus{ type: 'button', value: "+ Add A New Rule", ng: { click: 'addNewRuleTo(tagGroup)' } } + .add_tag + %input.button.icon-plus{ type: 'button', value: "+ Add A New Tag", ng: { click: 'addNewTag()' } } diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 8b972a3321..05498010bb 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -181,6 +181,58 @@ module Admin end end end + + describe "tag rules" do + let(:enterprise) { create(:distributor_enterprise) } + let!(:tag_rule) { create(:tag_rule, enterprise: enterprise) } + + before do + login_as_enterprise_user [enterprise] + end + + context "discount order rules" do + it "updates the existing rule with new attributes" do + spree_put :update, { + id: enterprise, + enterprise: { + tag_rules_attributes: { + '0' => { + id: tag_rule, + type: "TagRule::DiscountOrder", + preferred_customer_tags: "some,new,tags", + calculator_type: "Spree::Calculator::FlatPercentItemTotal", + calculator_attributes: { id: tag_rule.calculator.id, preferred_flat_percent: "15" } + } + } + } + } + tag_rule.reload + expect(tag_rule.preferred_customer_tags).to eq "some,new,tags" + expect(tag_rule.calculator.preferred_flat_percent).to eq 15 + end + + it "creates new rules with new attributes" do + spree_put :update, { + id: enterprise, + enterprise: { + tag_rules_attributes: { + '0' => { + id: "", + type: "TagRule::DiscountOrder", + preferred_customer_tags: "tags,are,awesome", + calculator_type: "Spree::Calculator::FlatPercentItemTotal", + calculator_attributes: { id: "", preferred_flat_percent: "24" } + } + } + } + } + expect(tag_rule.reload).to be + new_tag_rule = TagRule::DiscountOrder.last + expect(new_tag_rule.preferred_customer_tags).to eq "tags,are,awesome" + expect(new_tag_rule.calculator.preferred_flat_percent).to eq 24 + end + end + end end context "as owner" do diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 3e9008968b..2b77d6e18b 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -285,6 +285,32 @@ feature %q{ describe "tag rules", js: true do let!(:enterprise) { create(:distributor_enterprise) } + context "creating" do + before do + login_to_admin_section + visit main_app.edit_admin_enterprise_path(enterprise) + end + + it "creates a new rule" do + click_link "Tag Rules" + + expect(page).to_not have_selector '.customer_tag' + expect(page).to have_content 'No tags apply to this enterprise yet' + click_button '+ Add A New Tag' + find(:css, "tags-input .tags input").set "volunteer\n" + + expect(page).to have_content 'No rules apply to this tag yet' + click_button '+ Add A New Rule' + fill_in "enterprise[tag_rules_attributes][0][calculator_attributes][preferred_flat_percent]", with: 22 + + click_button 'Update' + + tag_rule = TagRule::DiscountOrder.last + expect(tag_rule.preferred_customer_tags).to eq "volunteer" + expect(tag_rule.calculator.preferred_flat_percent).to eq 22 + end + end + context "updating" do let!(:tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } @@ -296,12 +322,15 @@ feature %q{ it "saves changes to the rule" do click_link "Tag Rules" - expect(first('.customer_tag .header')).to have_content "For customers tagged #member" - expect(page).to have_input "enterprise[tag_rules_attributes][#{tag_rule.id}][calculator_attributes][preferred_flat_percent]", with: "0" - fill_in "enterprise[tag_rules_attributes][#{tag_rule.id}][calculator_attributes][preferred_flat_percent]", with: 45 + expect(first('.customer_tag .header')).to have_content "For customers tagged:" + expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" + find(:css, "tags-input .tags input").set "volunteer\n" + expect(page).to have_input "enterprise[tag_rules_attributes][0][calculator_attributes][preferred_flat_percent]", with: "0" + fill_in "enterprise[tag_rules_attributes][0][calculator_attributes][preferred_flat_percent]", with: 45 click_button 'Update' + expect(tag_rule.preferred_customer_tags).to eq "member,volunteer" expect(tag_rule.calculator.preferred_flat_percent).to eq 45 end end diff --git a/spec/javascripts/unit/admin/enterprises/controllers/tag_rules_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/tag_rules_controller_spec.js.coffee index 1b4ca0f34e..dd2fcee449 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/tag_rules_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/tag_rules_controller_spec.js.coffee @@ -6,10 +6,9 @@ describe "TagRulesCtrl", -> beforeEach -> module('admin.enterprises') enterprise = - tag_rules: [ - { id: 1, preferred_customer_tags: "member" }, - { id: 2, preferred_customer_tags: "member" }, - { id: 3, preferred_customer_tags: "local" } + tag_groups: [ + { tags: "member", rules: [{ id: 1, preferred_customer_tags: "member" }, { id: 2, preferred_customer_tags: "member" }] }, + { tags: "volunteer", rules: [{ id: 3, preferred_customer_tags: "local" }] } ] inject ($rootScope, $controller) -> @@ -17,9 +16,12 @@ describe "TagRulesCtrl", -> scope.Enterprise = enterprise ctrl = $controller 'TagRulesCtrl', {$scope: scope} - describe "initialization", -> - it "groups rules by preferred_customer_tags", -> - expect(scope.groupedTagRules).toEqual { - member: [{ id: 1, preferred_customer_tags: "member" }, { id: 2, preferred_customer_tags: "member" }], - local: [{ id: 3, preferred_customer_tags: "local" }] - } + describe "tagGroup start indices", -> + it "updates on initialization", -> + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 2 + + it "updates when tags are added to a tagGroup", -> + scope.addNewRuleTo(scope.tagGroups[0]) + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 3