From 30bb948d35f392cb59cd7983626cdbe03cd5997c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 15 Apr 2015 10:25:20 +1000 Subject: [PATCH 01/56] Making ofn-line-item-upd-attr work without ng-model Conflicts: app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee app/views/spree/admin/orders/bulk_management.html.haml --- .../directives/line_item_upd_attr.js.coffee | 41 +++---- .../admin/orders/bulk_management.html.haml | 107 ++++++++++-------- 2 files changed, 75 insertions(+), 73 deletions(-) diff --git a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee b/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee index c83d7fdc0f..a33fa6a1b9 100644 --- a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee +++ b/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee @@ -1,23 +1,18 @@ -angular.module("ofn.admin").directive "ofnLineItemUpdAttr", [ - "switchClass", "pendingChanges" - (switchClass, pendingChanges) -> - require: "ngModel" - link: (scope, element, attrs, ngModel) -> - attrName = attrs.ofnLineItemUpdAttr - element.dbValue = scope.$eval(attrs.ngModel) - scope.$watch -> - scope.$eval(attrs.ngModel) - , (value) -> - if ngModel.$dirty - if value == element.dbValue - pendingChanges.remove(scope.line_item.id, attrName) - switchClass( element, "", ["update-pending", "update-error", "update-success"], false ) - else - changeObj = - lineItem: scope.line_item - element: element - attrName: attrName - url: "/api/orders/#{scope.line_item.order.number}/line_items/#{scope.line_item.id}?line_item[#{attrName}]=#{value}" - pendingChanges.add(scope.line_item.id, attrName, changeObj) - switchClass( element, "update-pending", ["update-error", "update-success"], false ) -] \ No newline at end of file +angular.module("ofn.admin").directive "ofnLineItemUpdAttr", (switchClass, pendingChanges) -> + scope: + lineItem: "&ofnLineItemUpdAttr" + attrName: "@" + link: (scope, element, attrs) -> + element.dbValue = scope.lineItem()[scope.attrName] + scope.$watch "lineItem().#{scope.attrName}", (value) -> + if value == element.dbValue + pendingChanges.remove(scope.lineItem().id, scope.attrName) + switchClass( element, "", ["update-pending", "update-error", "update-success"], false ) + else + changeObj = + lineItem: scope.lineItem() + element: element + attrName: scope.attrName + url: "/api/orders/#{scope.lineItem().order.number}/line_items/#{scope.lineItem().id}?line_item[#{scope.attrName}]=#{value}" + pendingChanges.add(scope.lineItem().id, scope.attrName, changeObj) + switchClass( element, "update-pending", ["update-error", "update-success"], false ) diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 61bf9fbb7e..406dc18046 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -100,53 +100,60 @@ %div{ :class => "sixteen columns alpha", 'ng-show' => '!loading && filteredLineItems.length == 0'} %h1#no_results No orders found. %div{ 'ng-hide' => 'loading || filteredLineItems.length == 0' } - %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } - %thead - %tr - %th.bulk - %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } - %th.order_no{ 'ng-show' => 'columns.order_no.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.number'; reverse = !reverse" } Order No. - %th.full_name{ 'ng-show' => 'columns.full_name.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.full_name'; reverse = !reverse" } Name - %th.email{ 'ng-show' => 'columns.email.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.email'; reverse = !reverse" } Email - %th.phone{ 'ng-show' => 'columns.phone.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.phone'; reverse = !reverse" } Phone - %th.date{ 'ng-show' => 'columns.order_date.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.completed_at'; reverse = !reverse" } Order Date - %th.producer{ 'ng-show' => 'columns.producer.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'supplier.name'; reverse = !reverse" } Producer - %th.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.order_cycle.name'; reverse = !reverse" } Order Cycle - %th.hub{ 'ng-show' => 'columns.hub.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.distributor.name'; reverse = !reverse" } Hub - %th.variant{ 'ng-show' => 'columns.variant.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'units_variant.unit_text'; reverse = !reverse" } Product: Unit - %th.quantity{ 'ng-show' => 'columns.quantity.visible' } Quantity - %th.max{ 'ng-show' => 'columns.max.visible' } Max - %th.actions - %th.actions - Ask?  - %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } - %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } - %td.bulk - %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } - %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} - %td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }} - %td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }} - %td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }} - %td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }} - %td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }} - %td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }} - %td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }} - %td.variant{ 'ng-show' => 'columns.variant.visible' } - %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.unit_text }} - %td.quantity{ 'ng-show' => 'columns.quantity.visible' } - %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "quantity" } - %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} - %td.actions - %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } - %td.actions - %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } - %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' } \ No newline at end of file + %form{ 'ng-model' => "bulk_order_form" } + %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } + %thead + %tr + %th.bulk + %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } + %th.order_no{ 'ng-show' => 'columns.order_no.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.number'; reverse = !reverse" } Order No. + %th.full_name{ 'ng-show' => 'columns.full_name.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.full_name'; reverse = !reverse" } Name + %th.email{ 'ng-show' => 'columns.email.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.email'; reverse = !reverse" } Email + %th.phone{ 'ng-show' => 'columns.phone.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.phone'; reverse = !reverse" } Phone + %th.date{ 'ng-show' => 'columns.order_date.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.completed_at'; reverse = !reverse" } Order Date + %th.producer{ 'ng-show' => 'columns.producer.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'supplier.name'; reverse = !reverse" } Producer + %th.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.order_cycle.name'; reverse = !reverse" } Order Cycle + %th.hub{ 'ng-show' => 'columns.hub.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.distributor.name'; reverse = !reverse" } Hub + %th.variant{ 'ng-show' => 'columns.variant.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'units_variant.unit_text'; reverse = !reverse" } Product: Unit + %th.quantity{ 'ng-show' => 'columns.quantity.visible' } Quantity + %th.max{ 'ng-show' => 'columns.max.visible' } Max + %th.unit_value{ 'ng-show' => 'columns.unit_value.visible' } Weight/Volume + %th.price{ 'ng-show' => 'columns.price.visible' } Price + %th.actions + %th.actions + Ask?  + %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } + %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } + %td.bulk + %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } + %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} + %td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }} + %td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }} + %td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }} + %td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }} + %td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }} + %td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }} + %td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }} + %td.variant{ 'ng-show' => 'columns.variant.visible' } + %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.unit_text }} + %td.quantity{ 'ng-show' => 'columns.quantity.visible' } + %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "quantity" } + %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} + %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } + %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "unit_value" } + %td.price{ 'ng-show' => 'columns.price.visible' } + %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "price" } + %td.actions + %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } + %td.actions + %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } + %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' } From b16fa9cdc14e76494a150e10936800dff8b0c579 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 15 Apr 2015 11:29:51 +1000 Subject: [PATCH 02/56] Generalising datasubmitter pendingChanges and watcher directive --- .../directives/line_item_upd_attr.js.coffee | 46 +++++++++++------ .../admin/services/data_submitter.js.coffee | 24 +++++---- .../admin/services/pending_changes.js.coffee | 49 +++++++++---------- 3 files changed, 66 insertions(+), 53 deletions(-) diff --git a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee b/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee index a33fa6a1b9..a1652950e7 100644 --- a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee +++ b/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee @@ -1,18 +1,36 @@ angular.module("ofn.admin").directive "ofnLineItemUpdAttr", (switchClass, pendingChanges) -> scope: - lineItem: "&ofnLineItemUpdAttr" - attrName: "@" + object: "&ofnLineItemUpdAttr" + type: "@ofnLineItemUpdAttr" + attr: "@attrName" link: (scope, element, attrs) -> - element.dbValue = scope.lineItem()[scope.attrName] - scope.$watch "lineItem().#{scope.attrName}", (value) -> - if value == element.dbValue - pendingChanges.remove(scope.lineItem().id, scope.attrName) - switchClass( element, "", ["update-pending", "update-error", "update-success"], false ) + scope.savedValue = scope.object()[scope.attr] + + scope.$watch "object().#{scope.attr}", (value) -> + if value == scope.savedValue + pendingChanges.remove(scope.object().id, scope.attr) + scope.clear() else - changeObj = - lineItem: scope.lineItem() - element: element - attrName: scope.attrName - url: "/api/orders/#{scope.lineItem().order.number}/line_items/#{scope.lineItem().id}?line_item[#{scope.attrName}]=#{value}" - pendingChanges.add(scope.lineItem().id, scope.attrName, changeObj) - switchClass( element, "update-pending", ["update-error", "update-success"], false ) + change = + object: scope.object() + type: scope.type + attr: scope.attr + value: value + scope: scope + scope.pending() + pendingChanges.add(scope.object().id, scope.attr, change) + + scope.reset = (value) -> + scope.savedValue = value + + scope.success = -> + switchClass( element, "update-success", ["update-pending", "update-error"], 3000 ) + + scope.pending = -> + switchClass( element, "update-pending", ["update-error", "update-success"], false ) + + scope.error = -> + switchClass( element, "update-error", ["update-pending", "update-success"], false ) + + scope.clear = -> + switchClass( element, "", ["update-pending", "update-error", "update-success"], false ) diff --git a/app/assets/javascripts/admin/services/data_submitter.js.coffee b/app/assets/javascripts/admin/services/data_submitter.js.coffee index 7d121ec645..e6496a1303 100644 --- a/app/assets/javascripts/admin/services/data_submitter.js.coffee +++ b/app/assets/javascripts/admin/services/data_submitter.js.coffee @@ -1,13 +1,11 @@ -angular.module("ofn.admin").factory "dataSubmitter", [ - "$http", "$q", "switchClass" - ($http, $q, switchClass) -> - return (changeObj) -> - deferred = $q.defer() - $http.put(changeObj.url).success((data) -> - switchClass changeObj.element, "update-success", ["update-pending", "update-error"], 3000 - deferred.resolve data - ).error -> - switchClass changeObj.element, "update-error", ["update-pending", "update-success"], false - deferred.reject() - deferred.promise -] \ No newline at end of file +angular.module("ofn.admin").factory "dataSubmitter", ($http, $q) -> + return (change) -> + deferred = $q.defer() + url = "/api/orders/#{change.object.order.number}/line_items/#{change.object.id}?line_item[#{change.attr}]=#{change.value}" + $http.put(url).success((data) -> + change.scope.success() + deferred.resolve data + ).error -> + change.scope.error() + deferred.reject() + deferred.promise diff --git a/app/assets/javascripts/admin/services/pending_changes.js.coffee b/app/assets/javascripts/admin/services/pending_changes.js.coffee index d72a4ac7bc..64a463eb62 100644 --- a/app/assets/javascripts/admin/services/pending_changes.js.coffee +++ b/app/assets/javascripts/admin/services/pending_changes.js.coffee @@ -1,32 +1,29 @@ -angular.module("ofn.admin").factory "pendingChanges",[ - "dataSubmitter" - (dataSubmitter) -> - pendingChanges: {} +angular.module("ofn.admin").factory "pendingChanges", (dataSubmitter) -> + pendingChanges: {} - add: (id, attrName, changeObj) -> - @pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}") - @pendingChanges["#{id}"]["#{attrName}"] = changeObj + add: (id, attr, change) -> + @pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}") + @pendingChanges["#{id}"]["#{attr}"] = change - removeAll: -> - @pendingChanges = {} + removeAll: -> + @pendingChanges = {} - remove: (id, attrName) -> - if @pendingChanges.hasOwnProperty("#{id}") - delete @pendingChanges["#{id}"]["#{attrName}"] - delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 + remove: (id, attr) -> + if @pendingChanges.hasOwnProperty("#{id}") + delete @pendingChanges["#{id}"]["#{attr}"] + delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 - submitAll: -> - all = [] - for id,lineItem of @pendingChanges - for attrName,changeObj of lineItem - all.push @submit(id, attrName, changeObj) - all + submitAll: -> + all = [] + for id, objectChanges of @pendingChanges + for attrName, change of objectChanges + all.push @submit(change) + all - submit: (id, attrName, change) -> - dataSubmitter(change).then (data) => - @remove id, attrName - change.element.dbValue = data["#{attrName}"] + submit: (change) -> + dataSubmitter(change).then (data) => + @remove change.object.id, change.attr + change.scope.reset( data["#{change.attr}"] ) - changeCount: (lineItem) -> - Object.keys(lineItem).length -] \ No newline at end of file + changeCount: (objectChanges) -> + Object.keys(objectChanges).length From 57dbc33a7be52b5c4fbe45070e78cd0b399882a4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 16 Apr 2015 08:44:24 +1000 Subject: [PATCH 03/56] Adding resource service to determine submission request based on object type --- .../admin/services/data_submitter.js.coffee | 7 +++--- .../admin/services/resources.js.coffee | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/admin/services/resources.js.coffee diff --git a/app/assets/javascripts/admin/services/data_submitter.js.coffee b/app/assets/javascripts/admin/services/data_submitter.js.coffee index e6496a1303..a9b917aae2 100644 --- a/app/assets/javascripts/admin/services/data_submitter.js.coffee +++ b/app/assets/javascripts/admin/services/data_submitter.js.coffee @@ -1,11 +1,10 @@ -angular.module("ofn.admin").factory "dataSubmitter", ($http, $q) -> +angular.module("ofn.admin").factory "dataSubmitter", ($http, $q, resources) -> return (change) -> deferred = $q.defer() - url = "/api/orders/#{change.object.order.number}/line_items/#{change.object.id}?line_item[#{change.attr}]=#{change.value}" - $http.put(url).success((data) -> + resources.update(change).$promise.then (data) -> change.scope.success() deferred.resolve data - ).error -> + , -> change.scope.error() deferred.reject() deferred.promise diff --git a/app/assets/javascripts/admin/services/resources.js.coffee b/app/assets/javascripts/admin/services/resources.js.coffee new file mode 100644 index 0000000000..337f1c2601 --- /dev/null +++ b/app/assets/javascripts/admin/services/resources.js.coffee @@ -0,0 +1,22 @@ +angular.module("ofn.admin").factory "resources", ($resource) -> + LineItem = $resource '/api/orders/:order_number/line_items/:line_item_id.json', + { order_number: '@order_cycle_id', line_item_id: '@line_item_id'}, + 'update': { method: 'PUT' } + + return { + update: (change) -> + params = {} + data = {} + resource = null + + switch change.type + when "line_item" + resource = LineItem; + params.order_number = change.object.order.number + params.line_item_id = change.object.id + data.line_item = {} + data.line_item[change.attr] = change.value + else "" + + resource.update(params, data) + } From 37ff61d663fd69de8e0ac96c8f8122b52ae66585 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sat, 2 May 2015 19:51:54 +1000 Subject: [PATCH 04/56] Adding basic routing to display customer index page --- app/controllers/admin/customers_controller.rb | 4 ++++ app/models/spree/ability_decorator.rb | 2 ++ app/views/admin/customers/index.html.haml | 1 + config/routes.rb | 2 ++ spec/models/spree/ability_spec.rb | 8 ++++++++ 5 files changed, 17 insertions(+) create mode 100644 app/controllers/admin/customers_controller.rb create mode 100644 app/views/admin/customers/index.html.haml diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb new file mode 100644 index 0000000000..d663b4f0ea --- /dev/null +++ b/app/controllers/admin/customers_controller.rb @@ -0,0 +1,4 @@ +module Admin + class CustomersController < ResourceController + end +end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index b8539979c2..b5ad4ec07b 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -185,6 +185,8 @@ class AbilityDecorator # Reports page can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report + + can [:admin, :index], Customer end diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml new file mode 100644 index 0000000000..95b841ac50 --- /dev/null +++ b/app/views/admin/customers/index.html.haml @@ -0,0 +1 @@ +Customers diff --git a/config/routes.rb b/config/routes.rb index 4621ee4a35..aa49f225dc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,6 +79,8 @@ Openfoodnetwork::Application.routes.draw do resources :variant_overrides do post :bulk_update, on: :collection end + + resources :customers, only: [:index] end namespace :api do diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 0dc9364967..8ff1054f19 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -220,6 +220,10 @@ module Spree should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises], for: :report) end + it "should not be able to list customers" do + should_not have_ability([:admin, :index], for: Customer) + end + describe "order_cycles abilities" do context "where the enterprise is not in an order_cycle" do let!(:order_cycle) { create(:simple_order_cycle) } @@ -407,6 +411,10 @@ module Spree should_not have_ability([:sales_total, :users_and_enterprises], for: :report) end + it "should be able to list customers" do + should have_ability([:admin, :index], for: Customer) + end + context "for a given order_cycle" do let!(:order_cycle) { create(:simple_order_cycle) } let!(:exchange){ create(:exchange, incoming: false, order_cycle: order_cycle, receiver: d1, sender: order_cycle.coordinator) } From 220f42fcf2572382f22fc170be49e08d84916696 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 6 May 2015 19:15:38 +1000 Subject: [PATCH 05/56] Customers controller serializes data for json requests, just renders view without data for html --- app/controllers/admin/customers_controller.rb | 24 +++++++ .../spree/admin/base_controller_decorator.rb | 4 ++ .../api/admin/customer_serializer.rb | 3 + .../admin/customers_controller_spec.rb | 62 +++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 app/serializers/api/admin/customer_serializer.rb create mode 100644 spec/controllers/admin/customers_controller_spec.rb diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index d663b4f0ea..b14ca38296 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -1,4 +1,28 @@ module Admin class CustomersController < ResourceController + before_filter :load_managed_shops, only: :index, if: :html_request? + + def index + respond_to do |format| + format.html + format.json do + render json: ActiveModel::ArraySerializer.new( @collection, + each_serializer: Api::Admin::CustomerSerializer, spree_current_user: spree_current_user + ).to_json + end + end + end + + private + + def collection + return Customer.where("1=0") if html_request? || params[:enterprise_id].nil? + enterprise = Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id]) + Customer.of(enterprise) + end + + def load_managed_shops + @shops = Enterprise.managed_by(spree_current_user).is_distributor + end end end diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index 85904590c3..3fa6a5c5e1 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -58,4 +58,8 @@ Spree::Admin::BaseController.class_eval do "Until you set these up, customers will not be able to shop at this hub." end end + + def html_request? + request.format.html? + end end diff --git a/app/serializers/api/admin/customer_serializer.rb b/app/serializers/api/admin/customer_serializer.rb new file mode 100644 index 0000000000..84f32b6e8b --- /dev/null +++ b/app/serializers/api/admin/customer_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::CustomerSerializer < ActiveModel::Serializer + attributes :id, :email, :enterprise_id, :user_id, :code +end diff --git a/spec/controllers/admin/customers_controller_spec.rb b/spec/controllers/admin/customers_controller_spec.rb new file mode 100644 index 0000000000..ee23b90552 --- /dev/null +++ b/spec/controllers/admin/customers_controller_spec.rb @@ -0,0 +1,62 @@ +describe Admin::CustomersController, type: :controller do + include AuthenticationWorkflow + + describe "index" do + let(:enterprise) { create(:distributor_enterprise) } + let(:another_enterprise) { create(:distributor_enterprise) } + + context "html" do + before do + controller.stub spree_current_user: enterprise.owner + end + + it "returns an empty @collection" do + spree_get :index, format: :html + expect(assigns(:collection)).to eq [] + end + end + + context "json" do + let!(:customer) { create(:customer, enterprise: enterprise) } + + context "where I manage the enterprise" do + before do + controller.stub spree_current_user: enterprise.owner + end + + context "and enterprise_id is given in params" do + let(:params) { { format: :json, enterprise_id: enterprise.id } } + + it "scopes @collection to customers of that enterprise" do + spree_get :index, params + expect(assigns(:collection)).to eq [customer] + end + + it "serializes the data" do + expect(ActiveModel::ArraySerializer).to receive(:new) + spree_get :index, params + end + end + + context "and enterprise_id is not given in params" do + it "returns an empty collection" do + spree_get :index, format: :json + expect(assigns(:collection)).to eq [] + end + end + end + + context "and I do not manage the enterprise" do + before do + controller.stub spree_current_user: another_enterprise.owner + end + + it "returns an empty collection" do + spree_get :index, format: :json + expect(assigns(:collection)).to eq [] + end + end + end + + end +end From b6d63d40c6d877ce39f2c2b48f9ef289db1cb3ae Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 6 May 2015 22:35:43 +1000 Subject: [PATCH 06/56] Pulling columns logic out into its own services/controller in a new indexUtils module --- .../controllers/columns_controller.js.coffee | 4 ++++ .../admin/index_utils/index_utils.js.coffee | 1 + .../index_utils/services/columns.js.coffee | 8 ++++++++ .../columns_controller_spec.js.coffee | 17 +++++++++++++++++ .../index_utils/services/columns.js.coffee | 15 +++++++++++++++ 5 files changed, 45 insertions(+) create mode 100644 app/assets/javascripts/admin/index_utils/controllers/columns_controller.js.coffee create mode 100644 app/assets/javascripts/admin/index_utils/index_utils.js.coffee create mode 100644 app/assets/javascripts/admin/index_utils/services/columns.js.coffee create mode 100644 spec/javascripts/unit/admin/index_utils/controllers/columns_controller_spec.js.coffee create mode 100644 spec/javascripts/unit/admin/index_utils/services/columns.js.coffee diff --git a/app/assets/javascripts/admin/index_utils/controllers/columns_controller.js.coffee b/app/assets/javascripts/admin/index_utils/controllers/columns_controller.js.coffee new file mode 100644 index 0000000000..39556983b3 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/controllers/columns_controller.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.indexUtils").controller "ColumnsCtrl", ($scope, Columns) -> + $scope.columns = Columns.columns + $scope.predicate = "" + $scope.reverse = false diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee new file mode 100644 index 0000000000..eef7b0aa5f --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -0,0 +1 @@ +angular.module("admin.indexUtils", []) \ No newline at end of file diff --git a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee new file mode 100644 index 0000000000..46e7ccd3b8 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.indexUtils").factory 'Columns', -> + new class Columns + columns: {} + + setColumns: (columns) -> + @columns = {} + @columns[name] = column for name, column of columns + @columns diff --git a/spec/javascripts/unit/admin/index_utils/controllers/columns_controller_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/controllers/columns_controller_spec.js.coffee new file mode 100644 index 0000000000..5fd79e71bf --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/controllers/columns_controller_spec.js.coffee @@ -0,0 +1,17 @@ +describe "ColumnsCtrl", -> + ctrl = null + scope = null + Columns = null + + beforeEach -> + Columns = { columns: { name: { visible: true} } } + + module('admin.indexUtils') + inject ($controller, $rootScope) -> + scope = $rootScope + ctrl = $controller 'ColumnsCtrl', {$scope: scope, Columns: Columns} + + it "initialises data", -> + expect(scope.columns).toEqual Columns.columns + expect(scope.predicate).toEqual "" + expect(scope.reverse).toEqual false diff --git a/spec/javascripts/unit/admin/index_utils/services/columns.js.coffee b/spec/javascripts/unit/admin/index_utils/services/columns.js.coffee new file mode 100644 index 0000000000..0b6093f31d --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/services/columns.js.coffee @@ -0,0 +1,15 @@ +describe "Columns service", -> + Columns = null + + beforeEach -> + module 'admin.indexUtils' + + inject (_Columns_) -> + Columns = _Columns_ + + Columns.columns = ["something"] + + describe "setting columns", -> + it "sets resets @columns and copies each column of the provided object across", -> + Columns.setColumns({ name: { visible: true } }) + expect(Columns.columns).toEqual { name: { visible: true } } From b3d314cfdb19e136dc7bd70b6ef6d7c043497478 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 6 May 2015 22:37:00 +1000 Subject: [PATCH 07/56] Adding require line for index_utils module to all.js --- app/assets/javascripts/admin/all.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 9a07309352..4dfc52395c 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -20,6 +20,7 @@ //= require ./admin //= require ./enterprises/enterprises //= require ./enterprise_groups/enterprise_groups +//= require ./index_utils/index_utils //= require ./payment_methods/payment_methods //= require ./products/products //= require ./shipping_methods/shipping_methods From c00c93816c8e2fa7a5faa687d258c1ee2c9f3530 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 6 May 2015 22:58:58 +1000 Subject: [PATCH 08/56] Renaming spec file --- .../services/{columns.js.coffee => columns_spec.js.coffee} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/javascripts/unit/admin/index_utils/services/{columns.js.coffee => columns_spec.js.coffee} (100%) diff --git a/spec/javascripts/unit/admin/index_utils/services/columns.js.coffee b/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee similarity index 100% rename from spec/javascripts/unit/admin/index_utils/services/columns.js.coffee rename to spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee From 307302038aebf87925122576eabba2159fc94da6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 15:13:21 +1000 Subject: [PATCH 09/56] Adding customers controller, service and resource --- app/assets/javascripts/admin/all.js | 1 + .../customers_controller.js.coffee | 13 ++++++++ .../admin/customers/customers.js.coffee | 1 + .../services/customer_resource.js.coffee | 8 +++++ .../customers/services/customers.js.coffee | 14 +++++++++ .../customers_controller_spec.js.coffee | 25 +++++++++++++++ .../services/customers_spec.js.coffee | 31 +++++++++++++++++++ 7 files changed, 93 insertions(+) create mode 100644 app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee create mode 100644 app/assets/javascripts/admin/customers/customers.js.coffee create mode 100644 app/assets/javascripts/admin/customers/services/customer_resource.js.coffee create mode 100644 app/assets/javascripts/admin/customers/services/customers.js.coffee create mode 100644 spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee create mode 100644 spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 4dfc52395c..03999b3cf0 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -18,6 +18,7 @@ //= require admin/spree_paypal_express //= require ../shared/ng-infinite-scroll.min.js //= require ./admin +//= require ./customers/customers //= require ./enterprises/enterprises //= require ./enterprise_groups/enterprise_groups //= require ./index_utils/index_utils diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee new file mode 100644 index 0000000000..6bee8f993e --- /dev/null +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -0,0 +1,13 @@ +angular.module("admin.customers").controller "customersCtrl", ($scope, Customers, Columns, shops) -> + $scope.shop = null + $scope.shops = shops + + $scope.columns = Columns.setColumns + email: { name: "Email", visible: true } + code: { name: "Code", visible: true } + + $scope.initialise = -> + $scope.customers = Customers.index(enterprise_id: $scope.shop.id) + + $scope.loaded = -> + Customers.loaded diff --git a/app/assets/javascripts/admin/customers/customers.js.coffee b/app/assets/javascripts/admin/customers/customers.js.coffee new file mode 100644 index 0000000000..66be0e78a8 --- /dev/null +++ b/app/assets/javascripts/admin/customers/customers.js.coffee @@ -0,0 +1 @@ +angular.module("admin.customers", ['ngResource', 'admin.indexUtils']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee b/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee new file mode 100644 index 0000000000..523e0c1495 --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.customers").factory 'CustomerResource', ($resource) -> + $resource('/admin/customers.json', {}, { + 'index': + method: 'GET' + isArray: true + params: + enterprise_id: '@enterprise_id' + }) diff --git a/app/assets/javascripts/admin/customers/services/customers.js.coffee b/app/assets/javascripts/admin/customers/services/customers.js.coffee new file mode 100644 index 0000000000..2be0cfe8e8 --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/customers.js.coffee @@ -0,0 +1,14 @@ +angular.module("admin.customers").factory 'Customers', (CustomerResource) -> + new class Customers + customers: {} + loaded: false + + index: (params={}, callback=null) -> + CustomerResource.index params, (data) => + for customer in data + @customers[customer.id] = customer + + @loaded = true + (callback || angular.noop)(@customers) + + @customers diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee new file mode 100644 index 0000000000..ced19c8b33 --- /dev/null +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -0,0 +1,25 @@ +describe "CustomersCtrl", -> + ctrl = null + scope = null + Customers = null + + beforeEach -> + shops = "list of shops" + + module('admin.customers') + inject ($controller, $rootScope, _Customers_) -> + scope = $rootScope + Customers = _Customers_ + ctrl = $controller 'customersCtrl', {$scope: scope, Customers: Customers, shops: shops} + + describe "initialise()", -> + beforeEach -> + spyOn(Customers, "index").andReturn "list of customers" + scope.shop = {id: 1} + scope.initialise() + + it "calls Customers#index with the correct params", -> + expect(Customers.index).toHaveBeenCalledWith({enterprise_id: 1}) + + it "resets $scope.customers with the result of Customers#index", -> + expect(scope.customers).toEqual "list of customers" diff --git a/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee b/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee new file mode 100644 index 0000000000..04779e3247 --- /dev/null +++ b/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee @@ -0,0 +1,31 @@ +describe "Customers service", -> + Customers = CustomerResource = customers = $httpBackend = null + + beforeEach -> + module 'admin.customers' + + inject ($q, _$httpBackend_, _Customers_, _CustomerResource_) -> + Customers = _Customers_ + CustomerResource = _CustomerResource_ + $httpBackend = _$httpBackend_ + $httpBackend.expectGET('/admin/customers.json?enterprise_id=2').respond 200, [{ id: 5, email: 'someone@email.com'}] + + describe "#index", -> + result = null + + beforeEach -> + expect(Customers.loaded).toBe false + result = Customers.index(enterprise_id: 2) + $httpBackend.flush() + + it "stores returned data in @customers, with ids as keys", -> + # This is super weird and freaking annoying. I think resource results have extra + # properties ($then, $promise) that cause them to not be equal to the reponse object + # provided to the expectGET clause above. + expect(Customers.customers).toEqual { 5: new CustomerResource({ id: 5, email: 'someone@email.com'}) } + + it "returns @customers", -> + expect(result).toEqual Customers.customers + + it "sets @loaded to true", -> + expect(Customers.loaded).toBe true From 7a4f0e214cc10094775bd6a98742d01adcbe70e6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 16:24:58 +1000 Subject: [PATCH 10/56] Adding customers index view --- app/helpers/admin/injection_helper.rb | 4 +++ app/views/admin/customers/index.html.haml | 34 +++++++++++++++++++++- spec/factories.rb | 2 +- spec/features/admin/customers_spec.rb | 35 +++++++++++++++++++++++ spec/support/matchers/select2_matchers.rb | 9 ++++++ 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 spec/features/admin/customers_spec.rb diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 36ddccdb9a..70587863fd 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -25,6 +25,10 @@ module Admin admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer end + def admin_inject_shops + admin_inject_json_ams_array "admin.customers", "shops", @shops, Api::Admin::IdNameSerializer + end + def admin_inject_hubs admin_inject_json_ams_array "ofn.admin", "hubs", @hubs, Api::Admin::IdNameSerializer end diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index 95b841ac50..7bd45ee4c3 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -1 +1,33 @@ -Customers += admin_inject_shops + +%div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } } + .row{ ng: { hide: "loaded()" } } + .two.columns.alpha + Hub + .four.columns + %select.select2.fullwidth#shop_id{ 'ng-model' => 'shop.id', name: 'shop_id', 'ng-options' => 'shop.id as shop.name for shop in shops' } + .ten.columns.omega + %input{ type: 'button', value: 'Go', ng: { click: 'initialise()' } } + + .row{ ng: { show: "loaded()" } } + %form{ name: "customers" } + %table.index#customers{ :class => "sixteen columns alpha" } + %thead + %tr{ ng: { controller: "ColumnsCtrl" } } + %th.bulk + %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } + %th.email{ 'ng-show' => 'columns.email.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'customer.email'; reverse = !reverse" } Email + %th.code{ 'ng-show' => 'columns.code.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'customer.code'; reverse = !reverse" } Code + %th.actions + Ask?  + %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } + %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{id}}" } + %td.bulk + %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } + %td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }} + %td.code{ 'ng-show' => 'columns.code.visible' } {{ customer.code }} + %td.actions + %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } + -# %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' } diff --git a/spec/factories.rb b/spec/factories.rb index c798999134..7d09115a12 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -208,7 +208,7 @@ FactoryGirl.define do factory :customer, :class => Customer do email { Faker::Internet.email } enterprise - code 'abc123' + code { Faker::Lorem.word } user end end diff --git a/spec/features/admin/customers_spec.rb b/spec/features/admin/customers_spec.rb new file mode 100644 index 0000000000..3cc46e165d --- /dev/null +++ b/spec/features/admin/customers_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +feature 'Customers' do + include AuthenticationWorkflow + include WebHelper + + context "as an enterprise user" do + let(:user) { create_enterprise_user } + let(:managed_distributor) { create(:distributor_enterprise, owner: user) } + let(:unmanaged_distributor) { create(:distributor_enterprise) } + + describe "using the customers index" do + let!(:customer1) { create(:customer, enterprise: managed_distributor) } + let!(:customer2) { create(:customer, enterprise: unmanaged_distributor) } + + before do + quick_login_as user + visit admin_customers_path + end + + it "lists my customers", js: true do + # Prompts for a hub + expect(page).to have_select2 "shop_id", with_options: [managed_distributor.name], without_options: [unmanaged_distributor.name] + + select2_select managed_distributor.name, from: "shop_id" + click_button "Go" + + expect(page).to have_content customer1.email + expect(page).to have_content customer1.code + expect(page).to_not have_content customer2.email + expect(page).to_not have_content customer2.code + end + end + end +end diff --git a/spec/support/matchers/select2_matchers.rb b/spec/support/matchers/select2_matchers.rb index 67cfcd81d0..cf67c7c7cb 100644 --- a/spec/support/matchers/select2_matchers.rb +++ b/spec/support/matchers/select2_matchers.rb @@ -21,6 +21,7 @@ RSpec::Matchers.define :have_select2 do |id, options={}| if results.all? results << all_options_present(from, options[:with_options]) if options.key? :with_options results << exact_options_present(from, options[:options]) if options.key? :options + results << no_options_present(from, options[:without_options]) if options.key? :without_options end results.all? @@ -51,6 +52,14 @@ RSpec::Matchers.define :have_select2 do |id, options={}| end end + def no_options_present(from, options) + with_select2_open(from) do + options.none? do |option| + @node.has_selector? "div.select2-drop-active ul.select2-results li", text: option + end + end + end + def selected_option_is(from, text) within find(from) do find("a.select2-choice").text == text From 1c03e27686bb334fdf3f2981a661cc617e52cc3e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 16:43:05 +1000 Subject: [PATCH 11/56] Moving dropdown to its own folder --- app/assets/javascripts/admin/{ => dropdown}/dropdown.js.coffee | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/admin/{ => dropdown}/dropdown.js.coffee (100%) diff --git a/app/assets/javascripts/admin/dropdown.js.coffee b/app/assets/javascripts/admin/dropdown/dropdown.js.coffee similarity index 100% rename from app/assets/javascripts/admin/dropdown.js.coffee rename to app/assets/javascripts/admin/dropdown/dropdown.js.coffee From f5c0ae0f4192f3550502e558653a7eef37a5d54c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 16:48:37 +1000 Subject: [PATCH 12/56] Moving dropdown to its own module folder and renaming --- app/assets/javascripts/admin/admin.js.coffee | 2 +- app/assets/javascripts/admin/all.js | 1 + .../controllers/dropdown_controller.js.coffee | 2 ++ .../directives/close_on_click.js.coffee | 5 +++ .../dropdown/directives/dropdown.js.coffee | 20 ++++++++++++ .../admin/dropdown/dropdown.js.coffee | 32 +------------------ 6 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee create mode 100644 app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee create mode 100644 app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee diff --git a/app/assets/javascripts/admin/admin.js.coffee b/app/assets/javascripts/admin/admin.js.coffee index 1c9f65f91a..8a3de223fd 100644 --- a/app/assets/javascripts/admin/admin.js.coffee +++ b/app/assets/javascripts/admin/admin.js.coffee @@ -1,3 +1,3 @@ -angular.module("ofn.admin", ["ngResource", "ngAnimate", "ofn.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) -> +angular.module("ofn.admin", ["ngResource", "ngAnimate", "admin.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content") $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 03999b3cf0..f6d016dc9a 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -19,6 +19,7 @@ //= require ../shared/ng-infinite-scroll.min.js //= require ./admin //= require ./customers/customers +//= require ./dropdown/dropdown //= require ./enterprises/enterprises //= require ./enterprise_groups/enterprise_groups //= require ./index_utils/index_utils diff --git a/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee b/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee new file mode 100644 index 0000000000..02e47ff9f7 --- /dev/null +++ b/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee @@ -0,0 +1,2 @@ +angular.module("admin.dropdown").controller "DropDownCtrl", ($scope) -> + $scope.expanded = false diff --git a/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee b/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee new file mode 100644 index 0000000000..9b506cb8fb --- /dev/null +++ b/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee @@ -0,0 +1,5 @@ + angular.module("admin.dropdown").directive "ofnCloseOnClick", ($document) -> + link: (scope, element, attrs) -> + element.click (event) -> + event.stopPropagation() + scope.$emit "offClick" diff --git a/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee b/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee new file mode 100644 index 0000000000..560598d23e --- /dev/null +++ b/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee @@ -0,0 +1,20 @@ + angular.module("admin.dropdown").directive "ofnDropDown", ($document) -> + link: (scope, element, attrs) -> + outsideClickListener = (event) -> + unless $(event.target).is("div.ofn_drop_down##{attrs.id} div.menu") || + $(event.target).parents("div.ofn_drop_down##{attrs.id} div.menu").length > 0 + scope.$emit "offClick" + + element.click (event) -> + if !scope.expanded + event.stopPropagation() + scope.deregistrationCallback = scope.$on "offClick", -> + $document.off "click", outsideClickListener + scope.$apply -> + scope.expanded = false + element.removeClass "expanded" + scope.deregistrationCallback() + $document.on "click", outsideClickListener + scope.$apply -> + scope.expanded = true + element.addClass "expanded" diff --git a/app/assets/javascripts/admin/dropdown/dropdown.js.coffee b/app/assets/javascripts/admin/dropdown/dropdown.js.coffee index e18407abcc..ae6118390f 100644 --- a/app/assets/javascripts/admin/dropdown/dropdown.js.coffee +++ b/app/assets/javascripts/admin/dropdown/dropdown.js.coffee @@ -1,31 +1 @@ -dropDownModule = angular.module("ofn.dropdown", []) - -dropDownModule.directive "ofnDropDown", ($document) -> - link: (scope, element, attrs) -> - outsideClickListener = (event) -> - unless $(event.target).is("div.ofn_drop_down##{attrs.id} div.menu") || - $(event.target).parents("div.ofn_drop_down##{attrs.id} div.menu").length > 0 - scope.$emit "offClick" - - element.click (event) -> - if !scope.expanded - event.stopPropagation() - scope.deregistrationCallback = scope.$on "offClick", -> - $document.off "click", outsideClickListener - scope.$apply -> - scope.expanded = false - element.removeClass "expanded" - scope.deregistrationCallback() - $document.on "click", outsideClickListener - scope.$apply -> - scope.expanded = true - element.addClass "expanded" - -dropDownModule.directive "ofnCloseOnClick", ($document) -> - link: (scope, element, attrs) -> - element.click (event) -> - event.stopPropagation() - scope.$emit "offClick" - -dropDownModule.controller "DropDownCtrl", ($scope) -> - $scope.expanded = false \ No newline at end of file +angular.module("admin.dropdown", []) From 5bd842fe1a26d3831ce89593341a569ac9c1d9ca Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 16:54:59 +1000 Subject: [PATCH 13/56] Moving toggle_column directive to dropdown module --- .../admin/{ => dropdown}/directives/toggle_column.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename app/assets/javascripts/admin/{ => dropdown}/directives/toggle_column.js.coffee (74%) diff --git a/app/assets/javascripts/admin/directives/toggle_column.js.coffee b/app/assets/javascripts/admin/dropdown/directives/toggle_column.js.coffee similarity index 74% rename from app/assets/javascripts/admin/directives/toggle_column.js.coffee rename to app/assets/javascripts/admin/dropdown/directives/toggle_column.js.coffee index 1b8487eeb1..2129562e78 100644 --- a/app/assets/javascripts/admin/directives/toggle_column.js.coffee +++ b/app/assets/javascripts/admin/dropdown/directives/toggle_column.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").directive "ofnToggleColumn", -> +angular.module("admin.dropdown").directive "ofnToggleColumn", -> link: (scope, element, attrs) -> element.addClass "selected" if scope.column.visible element.click "click", -> @@ -8,4 +8,4 @@ angular.module("ofn.admin").directive "ofnToggleColumn", -> element.removeClass "selected" else scope.column.visible = true - element.addClass "selected" \ No newline at end of file + element.addClass "selected" From 69ba8540c1559a1b56812d25fbd3e25a572d31ad Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 16:57:02 +1000 Subject: [PATCH 14/56] Moving toggle_column directive to index_utils --- .../directives/toggle_column.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/assets/javascripts/admin/{dropdown => index_utils}/directives/toggle_column.js.coffee (83%) diff --git a/app/assets/javascripts/admin/dropdown/directives/toggle_column.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee similarity index 83% rename from app/assets/javascripts/admin/dropdown/directives/toggle_column.js.coffee rename to app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee index 2129562e78..d7871421fa 100644 --- a/app/assets/javascripts/admin/dropdown/directives/toggle_column.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.dropdown").directive "ofnToggleColumn", -> +angular.module("admin.index_utils").directive "ofnToggleColumn", -> link: (scope, element, attrs) -> element.addClass "selected" if scope.column.visible element.click "click", -> From 3b7ab086c9362c6234461800b807247c34eebbdc Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 17:04:42 +1000 Subject: [PATCH 15/56] Fix module name --- .../admin/index_utils/directives/toggle_column.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee index d7871421fa..d6239ff47f 100644 --- a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.index_utils").directive "ofnToggleColumn", -> +angular.module("admin.indexUtils").directive "ofnToggleColumn", -> link: (scope, element, attrs) -> element.addClass "selected" if scope.column.visible element.click "click", -> From 3e7d331892a81dc759e0ec15bf1b8613b64e1f9f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 17:05:26 +1000 Subject: [PATCH 16/56] BOM uses columns controller from index utils module --- app/assets/javascripts/admin/admin.js.coffee | 2 +- .../admin/bulk_order_management.js.coffee | 8 ++- .../admin/orders/bulk_management.html.haml | 50 +++++++++---------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/app/assets/javascripts/admin/admin.js.coffee b/app/assets/javascripts/admin/admin.js.coffee index 8a3de223fd..ffe2c9eaaa 100644 --- a/app/assets/javascripts/admin/admin.js.coffee +++ b/app/assets/javascripts/admin/admin.js.coffee @@ -1,3 +1,3 @@ -angular.module("ofn.admin", ["ngResource", "ngAnimate", "admin.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) -> +angular.module("ofn.admin", ["ngResource", "ngAnimate", "admin.indexUtils", "admin.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content") $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index 4c1a319c1a..3cb33e8432 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -1,6 +1,6 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ - "$scope", "$http", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey" - ($scope, $http, dataFetcher, blankOption, pendingChanges, VariantUnitManager, OptionValueNamer, SpreeApiKey) -> + "$scope", "$http", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey", "Columns" + ($scope, $http, dataFetcher, blankOption, pendingChanges, VariantUnitManager, OptionValueNamer, SpreeApiKey, Columns) -> $scope.loading = true $scope.initialiseVariables = -> @@ -18,9 +18,7 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ $scope.selectedUnitsProduct = {}; $scope.selectedUnitsVariant = {}; $scope.sharedResource = false - $scope.predicate = "" - $scope.reverse = false - $scope.columns = + $scope.columns = Columns.setColumns order_no: { name: "Order No.", visible: false } full_name: { name: "Name", visible: true } email: { name: "Email", visible: false } diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 406dc18046..c8ca7dd740 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -103,7 +103,7 @@ %form{ 'ng-model' => "bulk_order_form" } %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } %thead - %tr + %tr{ ng: { controller: "ColumnsCtrl" } } %th.bulk %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } %th.order_no{ 'ng-show' => 'columns.order_no.visible' } @@ -132,28 +132,28 @@ %th.actions Ask?  %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } - %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } - %td.bulk - %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } - %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} - %td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }} - %td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }} - %td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }} - %td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }} - %td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }} - %td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }} - %td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }} - %td.variant{ 'ng-show' => 'columns.variant.visible' } - %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.unit_text }} - %td.quantity{ 'ng-show' => 'columns.quantity.visible' } - %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "quantity" } - %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} - %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } - %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "unit_value" } - %td.price{ 'ng-show' => 'columns.price.visible' } - %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "price" } - %td.actions - %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } - %td.actions - %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } + %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } + %td.bulk + %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } + %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} + %td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }} + %td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }} + %td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }} + %td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }} + %td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }} + %td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }} + %td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }} + %td.variant{ 'ng-show' => 'columns.variant.visible' } + %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.unit_text }} + %td.quantity{ 'ng-show' => 'columns.quantity.visible' } + %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "quantity" } + %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} + %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } + %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "unit_value" } + %td.price{ 'ng-show' => 'columns.price.visible' } + %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "price" } + %td.actions + %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } + %td.actions + %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' } From 79d50a64aec5b8a2ead190040f783fa715121dfc Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 17:08:44 +1000 Subject: [PATCH 17/56] BPE uses columns controller from intex utils --- app/assets/javascripts/admin/bulk_product_update.js.coffee | 4 ++-- .../spree/admin/products/bulk_edit/_products_head.html.haml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 8cac0abeda..422199ad28 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,9 +1,9 @@ -angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth) -> +angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, Columns) -> $scope.loading = true $scope.StatusMessage = StatusMessage - $scope.columns = + $scope.columns = Columns.setColumns producer: {name: "Producer", visible: true} sku: {name: "SKU", visible: false} name: {name: "Name", visible: true} diff --git a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml index 2e37da2bc8..88bccea61b 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml @@ -15,7 +15,7 @@ %col.actions %thead - %tr + %tr{ ng: { controller: "ColumnsCtrl" } } %th.left-actions %th.producer{ 'ng-show' => 'columns.producer.visible' } Producer %th.sku{ 'ng-show' => 'columns.sku.visible' } SKU From 8f35ccf0072df6e2544504a5ae09fb095cc02438 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 7 May 2015 18:00:12 +1000 Subject: [PATCH 18/56] Adding quick search and column dropdown to customers index --- .../admin/customers/customers.js.coffee | 2 +- .../customers/services/customers.js.coffee | 6 ++-- app/views/admin/customers/index.html.haml | 30 ++++++++++++++++++- spec/features/admin/customers_spec.rb | 23 +++++++++++--- .../services/customers_spec.js.coffee | 2 +- 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/customers/customers.js.coffee b/app/assets/javascripts/admin/customers/customers.js.coffee index 66be0e78a8..462b7d5818 100644 --- a/app/assets/javascripts/admin/customers/customers.js.coffee +++ b/app/assets/javascripts/admin/customers/customers.js.coffee @@ -1 +1 @@ -angular.module("admin.customers", ['ngResource', 'admin.indexUtils']) \ No newline at end of file +angular.module("admin.customers", ['ngResource', 'admin.indexUtils', 'admin.dropdown']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/customers/services/customers.js.coffee b/app/assets/javascripts/admin/customers/services/customers.js.coffee index 2be0cfe8e8..9acfa317d2 100644 --- a/app/assets/javascripts/admin/customers/services/customers.js.coffee +++ b/app/assets/javascripts/admin/customers/services/customers.js.coffee @@ -1,12 +1,14 @@ angular.module("admin.customers").factory 'Customers', (CustomerResource) -> new class Customers - customers: {} + customers: [] + customers_by_id: {} loaded: false index: (params={}, callback=null) -> CustomerResource.index params, (data) => for customer in data - @customers[customer.id] = customer + @customers.push customer + @customers_by_id[customer.id] = customer @loaded = true (callback || angular.noop)(@customers) diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index 7bd45ee4c3..aef398c991 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -9,6 +9,34 @@ .ten.columns.omega %input{ type: 'button', value: 'Go', ng: { click: 'initialise()' } } + .row{ 'ng-hide' => '!loaded() || lineItems.length == 0' } + .controls{ :class => "sixteen columns alpha", :style => "margin-bottom: 15px;" } + .three.columns.alpha + %input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } + .three.columns   + -# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true } + -# %span{ :class => 'icon-check' }   Actions + -# %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + -# %div.menu{ 'ng-show' => "expanded" } + -# %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "action in bulkActions", 'ng-click' => "selectedBulkAction.callback(filteredCustomers)", 'ofn-close-on-click' => true } + -# %span{ :class => 'three columns omega' } {{action.name }} + .seven.columns   + .three.columns.omega + %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' } + %span{ :class => 'icon-reorder' }   Columns + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + %div.menu{ 'ng-show' => "expanded" } + %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true } + %span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "✓" || !column.visible && " " }} + %span{ :class => 'two columns omega' } {{column.name }} + .row + .sixteen.columns.alpha#loading{ 'ng-if' => 'shop_id && !loaded()' } + %img.spinner{ src: "/assets/spinning-circles.svg" } + %h1 LOADING CUSTOMERS + .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredCustomers.length == 0'} + %h1#no_results No customers found. + + .row{ ng: { show: "loaded()" } } %form{ name: "customers" } %table.index#customers{ :class => "sixteen columns alpha" } @@ -23,7 +51,7 @@ %th.actions Ask?  %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } - %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{id}}" } + %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" } %td.bulk %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } %td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }} diff --git a/spec/features/admin/customers_spec.rb b/spec/features/admin/customers_spec.rb index 3cc46e165d..38af3145f3 100644 --- a/spec/features/admin/customers_spec.rb +++ b/spec/features/admin/customers_spec.rb @@ -11,7 +11,8 @@ feature 'Customers' do describe "using the customers index" do let!(:customer1) { create(:customer, enterprise: managed_distributor) } - let!(:customer2) { create(:customer, enterprise: unmanaged_distributor) } + let!(:customer2) { create(:customer, enterprise: managed_distributor) } + let!(:customer3) { create(:customer, enterprise: unmanaged_distributor) } before do quick_login_as user @@ -25,10 +26,24 @@ feature 'Customers' do select2_select managed_distributor.name, from: "shop_id" click_button "Go" + # Loads the right customers + expect(page).to have_selector "tr#c_#{customer1.id}" + expect(page).to have_selector "tr#c_#{customer2.id}" + expect(page).to_not have_selector "tr#c_#{customer3.id}" + + # Searching + fill_in "quick_search", with: customer2.email + expect(page).to_not have_selector "tr#c_#{customer1.id}" + expect(page).to have_selector "tr#c_#{customer2.id}" + fill_in "quick_search", with: "" + + # Toggling columns + expect(page).to have_selector "th.email" expect(page).to have_content customer1.email - expect(page).to have_content customer1.code - expect(page).to_not have_content customer2.email - expect(page).to_not have_content customer2.code + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Email").click + expect(page).to_not have_selector "th.email" + expect(page).to_not have_content customer1.email end end end diff --git a/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee b/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee index 04779e3247..7123055d63 100644 --- a/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee @@ -22,7 +22,7 @@ describe "Customers service", -> # This is super weird and freaking annoying. I think resource results have extra # properties ($then, $promise) that cause them to not be equal to the reponse object # provided to the expectGET clause above. - expect(Customers.customers).toEqual { 5: new CustomerResource({ id: 5, email: 'someone@email.com'}) } + expect(Customers.customers).toEqual [ new CustomerResource({ id: 5, email: 'someone@email.com'}) ] it "returns @customers", -> expect(result).toEqual Customers.customers From 2afd501af3efc933f300bdcd3172b79bb5c9bcb5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 10:38:02 +1000 Subject: [PATCH 19/56] Wiring up update action for customers controller --- app/models/spree/ability_decorator.rb | 2 +- config/routes.rb | 2 +- .../admin/customers_controller_spec.rb | 33 +++++++++++++++++++ spec/models/spree/ability_spec.rb | 8 ++--- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index b5ad4ec07b..e7e8d9feb8 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -186,7 +186,7 @@ class AbilityDecorator # Reports page can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report - can [:admin, :index], Customer + can [:admin, :index, :update], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) end diff --git a/config/routes.rb b/config/routes.rb index aa49f225dc..e262813cb7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,7 +80,7 @@ Openfoodnetwork::Application.routes.draw do post :bulk_update, on: :collection end - resources :customers, only: [:index] + resources :customers, only: [:index, :update] end namespace :api do diff --git a/spec/controllers/admin/customers_controller_spec.rb b/spec/controllers/admin/customers_controller_spec.rb index ee23b90552..3fc5451b8b 100644 --- a/spec/controllers/admin/customers_controller_spec.rb +++ b/spec/controllers/admin/customers_controller_spec.rb @@ -57,6 +57,39 @@ describe Admin::CustomersController, type: :controller do end end end + end + describe "update" do + let(:enterprise) { create(:distributor_enterprise) } + let(:another_enterprise) { create(:distributor_enterprise) } + + context "json" do + let!(:customer) { create(:customer, enterprise: enterprise) } + + context "where I manage the customer's enterprise" do + before do + controller.stub spree_current_user: enterprise.owner + end + + it "allows me to update the customer" do + spree_put :update, format: :json, id: customer.id, customer: { email: 'new.email@gmail.com' } + expect(assigns(:customer)).to eq customer + expect(customer.reload.email).to eq 'new.email@gmail.com' + end + end + + context "where I don't manage the customer's enterprise" do + before do + controller.stub spree_current_user: another_enterprise.owner + end + + it "prevents me from updating the customer" do + spree_put :update, format: :json, id: customer.id, customer: { email: 'new.email@gmail.com' } + expect(response).to redirect_to spree.unauthorized_path + expect(assigns(:customer)).to eq nil + expect(customer.email).to_not eq 'new.email@gmail.com' + end + end + end end end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 8ff1054f19..e35291ec8e 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -220,8 +220,8 @@ module Spree should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises], for: :report) end - it "should not be able to list customers" do - should_not have_ability([:admin, :index], for: Customer) + it "should not be able to access customer actions" do + should_not have_ability([:admin, :index, :update], for: Customer) end describe "order_cycles abilities" do @@ -411,8 +411,8 @@ module Spree should_not have_ability([:sales_total, :users_and_enterprises], for: :report) end - it "should be able to list customers" do - should have_ability([:admin, :index], for: Customer) + it "should be able to access customer actions" do + should have_ability([:admin, :index, :update], for: Customer) end context "for a given order_cycle" do From a87327888157388a61882b83e7a6247e7d4a2425 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 10:53:59 +1000 Subject: [PATCH 20/56] Moving line-item-upd-attr to indexUtils module, renaming to obj-for-update --- .../directives/obj_for_update.js.coffee} | 8 ++++---- app/views/spree/admin/orders/bulk_management.html.haml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) rename app/assets/javascripts/admin/{directives/line_item_upd_attr.js.coffee => index_utils/directives/obj_for_update.js.coffee} (85%) diff --git a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee b/app/assets/javascripts/admin/index_utils/directives/obj_for_update.js.coffee similarity index 85% rename from app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee rename to app/assets/javascripts/admin/index_utils/directives/obj_for_update.js.coffee index a1652950e7..254fa5c438 100644 --- a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/obj_for_update.js.coffee @@ -1,8 +1,8 @@ -angular.module("ofn.admin").directive "ofnLineItemUpdAttr", (switchClass, pendingChanges) -> +angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendingChanges) -> scope: - object: "&ofnLineItemUpdAttr" - type: "@ofnLineItemUpdAttr" - attr: "@attrName" + object: "&objForUpdate" + type: "@objForUpdate" + attr: "@attrForUpdate" link: (scope, element, attrs) -> scope.savedValue = scope.object()[scope.attr] diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index c8ca7dd740..a2fc15e271 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -146,12 +146,12 @@ %td.variant{ 'ng-show' => 'columns.variant.visible' } %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.unit_text }} %td.quantity{ 'ng-show' => 'columns.quantity.visible' } - %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "quantity" } + %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'obj-for-update' => "line_item", "attr-for-update" => "quantity" } %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } - %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "unit_value" } + %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'obj-for-update' => "line_item", "attr-for-update" => "unit_value" } %td.price{ 'ng-show' => 'columns.price.visible' } - %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'ofn-line-item-upd-attr' => "line_item", "attr-name" => "price" } + %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'obj-for-update' => "line_item", "attr-for-update" => "price" } %td.actions %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } %td.actions From 8f94390363b92a80cd4bc1005cb3e02228ab07a8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 11:12:20 +1000 Subject: [PATCH 21/56] Moving a bunch of index related services from general admin module into indexUtils --- .../admin/index_utils/index_utils.js.coffee | 2 +- .../services/data_submitter.js.coffee | 2 +- .../services/pending_changes.js.coffee | 2 +- .../{ => index_utils}/services/resources.js.coffee | 2 +- .../index_utils/services/switch_class.js.coffee | 10 ++++++++++ .../admin/services/switch_class.js.coffee | 13 ------------- 6 files changed, 14 insertions(+), 17 deletions(-) rename app/assets/javascripts/admin/{ => index_utils}/services/data_submitter.js.coffee (73%) rename app/assets/javascripts/admin/{ => index_utils}/services/pending_changes.js.coffee (91%) rename app/assets/javascripts/admin/{ => index_utils}/services/resources.js.coffee (89%) create mode 100644 app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee delete mode 100644 app/assets/javascripts/admin/services/switch_class.js.coffee diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee index eef7b0aa5f..46fc050742 100644 --- a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -1 +1 @@ -angular.module("admin.indexUtils", []) \ No newline at end of file +angular.module("admin.indexUtils", ['ngResource']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/services/data_submitter.js.coffee b/app/assets/javascripts/admin/index_utils/services/data_submitter.js.coffee similarity index 73% rename from app/assets/javascripts/admin/services/data_submitter.js.coffee rename to app/assets/javascripts/admin/index_utils/services/data_submitter.js.coffee index a9b917aae2..934ae4a07e 100644 --- a/app/assets/javascripts/admin/services/data_submitter.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/data_submitter.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").factory "dataSubmitter", ($http, $q, resources) -> +angular.module("admin.indexUtils").factory "dataSubmitter", ($http, $q, resources) -> return (change) -> deferred = $q.defer() resources.update(change).$promise.then (data) -> diff --git a/app/assets/javascripts/admin/services/pending_changes.js.coffee b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee similarity index 91% rename from app/assets/javascripts/admin/services/pending_changes.js.coffee rename to app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee index 64a463eb62..f39a1b3ca6 100644 --- a/app/assets/javascripts/admin/services/pending_changes.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").factory "pendingChanges", (dataSubmitter) -> +angular.module("admin.indexUtils").factory "pendingChanges", (dataSubmitter) -> pendingChanges: {} add: (id, attr, change) -> diff --git a/app/assets/javascripts/admin/services/resources.js.coffee b/app/assets/javascripts/admin/index_utils/services/resources.js.coffee similarity index 89% rename from app/assets/javascripts/admin/services/resources.js.coffee rename to app/assets/javascripts/admin/index_utils/services/resources.js.coffee index 337f1c2601..c14744701b 100644 --- a/app/assets/javascripts/admin/services/resources.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/resources.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").factory "resources", ($resource) -> +angular.module("admin.indexUtils").factory "resources", ($resource) -> LineItem = $resource '/api/orders/:order_number/line_items/:line_item_id.json', { order_number: '@order_cycle_id', line_item_id: '@line_item_id'}, 'update': { method: 'PUT' } diff --git a/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee b/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee new file mode 100644 index 0000000000..c2a3419e2c --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee @@ -0,0 +1,10 @@ +angular.module("admin.indexUtils").factory "switchClass", ($timeout) -> + return (element,classToAdd,removeClasses,timeout) -> + $timeout.cancel element.timeout if element.timeout + element.removeClass className for className in removeClasses + element.addClass classToAdd + intRegex = /^\d+$/ + if timeout && intRegex.test(timeout) + element.timeout = $timeout(-> + element.removeClass classToAdd + , timeout, true) diff --git a/app/assets/javascripts/admin/services/switch_class.js.coffee b/app/assets/javascripts/admin/services/switch_class.js.coffee deleted file mode 100644 index e39c52d1f6..0000000000 --- a/app/assets/javascripts/admin/services/switch_class.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -angular.module("ofn.admin").factory "switchClass", [ - "$timeout" - ($timeout) -> - return (element,classToAdd,removeClasses,timeout) -> - $timeout.cancel element.timeout if element.timeout - element.removeClass className for className in removeClasses - element.addClass classToAdd - intRegex = /^\d+$/ - if timeout && intRegex.test(timeout) - element.timeout = $timeout(-> - element.removeClass classToAdd - , timeout, true) -] \ No newline at end of file From 3890ba9a11f88527c28638c418b94bcf4d01f744 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 12:34:41 +1000 Subject: [PATCH 22/56] Wrapping pending changes service in a class --- .../services/pending_changes.js.coffee | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee index f39a1b3ca6..86a6b2821f 100644 --- a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee @@ -1,29 +1,30 @@ angular.module("admin.indexUtils").factory "pendingChanges", (dataSubmitter) -> - pendingChanges: {} + new class pendingChanges + pendingChanges: {} - add: (id, attr, change) -> - @pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}") - @pendingChanges["#{id}"]["#{attr}"] = change + add: (id, attr, change) => + @pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}") + @pendingChanges["#{id}"]["#{attr}"] = change - removeAll: -> - @pendingChanges = {} + removeAll: => + @pendingChanges = {} - remove: (id, attr) -> - if @pendingChanges.hasOwnProperty("#{id}") - delete @pendingChanges["#{id}"]["#{attr}"] - delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 + remove: (id, attr) => + if @pendingChanges.hasOwnProperty("#{id}") + delete @pendingChanges["#{id}"]["#{attr}"] + delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 - submitAll: -> - all = [] - for id, objectChanges of @pendingChanges - for attrName, change of objectChanges - all.push @submit(change) - all + submitAll: => + all = [] + for id, objectChanges of @pendingChanges + for attrName, change of objectChanges + all.push @submit(change) + all - submit: (change) -> - dataSubmitter(change).then (data) => - @remove change.object.id, change.attr - change.scope.reset( data["#{change.attr}"] ) + submit: (change) -> + dataSubmitter(change).then (data) => + @remove change.object.id, change.attr + change.scope.reset( data["#{change.attr}"] ) - changeCount: (objectChanges) -> - Object.keys(objectChanges).length + changeCount: (objectChanges) -> + Object.keys(objectChanges).length From 4545e0ff95caaede5c04761c305ed7810db2caa0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 12:35:20 +1000 Subject: [PATCH 23/56] Adding request defaults to index utils module --- app/assets/javascripts/admin/index_utils/index_utils.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee index 46fc050742..0925bf45ed 100644 --- a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -1 +1 @@ -angular.module("admin.indexUtils", ['ngResource']) \ No newline at end of file +angular.module("admin.indexUtils", ['ngResource']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file From dcde2d88ad4ef73ec2cecd0804db0e12a06e04b8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 12:35:57 +1000 Subject: [PATCH 24/56] Adding Customer resource to resources service --- .../admin/index_utils/services/resources.js.coffee | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/index_utils/services/resources.js.coffee b/app/assets/javascripts/admin/index_utils/services/resources.js.coffee index c14744701b..65dad204d4 100644 --- a/app/assets/javascripts/admin/index_utils/services/resources.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/resources.js.coffee @@ -1,6 +1,9 @@ angular.module("admin.indexUtils").factory "resources", ($resource) -> LineItem = $resource '/api/orders/:order_number/line_items/:line_item_id.json', - { order_number: '@order_cycle_id', line_item_id: '@line_item_id'}, + { order_number: '@order_number', line_item_id: '@line_item_id'}, + 'update': { method: 'PUT' } + Customer = $resource '/admin/customers/:customer_id.json', + { customer_id: '@customer_id'}, 'update': { method: 'PUT' } return { @@ -11,11 +14,16 @@ angular.module("admin.indexUtils").factory "resources", ($resource) -> switch change.type when "line_item" - resource = LineItem; + resource = LineItem params.order_number = change.object.order.number params.line_item_id = change.object.id data.line_item = {} data.line_item[change.attr] = change.value + when "customer" + resource = Customer + params.customer_id = change.object.id + data.customer = {} + data.customer[change.attr] = change.value else "" resource.update(params, data) From 734ad21e82445630b706496e0a6a415f6fae0c18 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 12:36:56 +1000 Subject: [PATCH 25/56] Customers controller responds with json --- app/controllers/admin/customers_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index b14ca38296..27b5b1380d 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -1,6 +1,7 @@ module Admin class CustomersController < ResourceController before_filter :load_managed_shops, only: :index, if: :html_request? + respond_to :json def index respond_to do |format| From 9add073b171c6c98a7182a2dce3f9fefcd1a7ba5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 12:46:37 +1000 Subject: [PATCH 26/56] User can update customer code from customer index page --- .../controllers/customers_controller.js.coffee | 3 ++- app/views/admin/customers/index.html.haml | 17 +++++++++-------- spec/features/admin/customers_spec.rb | 11 ++++++++++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index 6bee8f993e..5f078525d9 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -1,6 +1,7 @@ -angular.module("admin.customers").controller "customersCtrl", ($scope, Customers, Columns, shops) -> +angular.module("admin.customers").controller "customersCtrl", ($scope, Customers, Columns, pendingChanges, shops) -> $scope.shop = null $scope.shops = shops + $scope.submitAll = pendingChanges.submitAll $scope.columns = Columns.setColumns email: { name: "Email", visible: true } diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index aef398c991..a8e6141796 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -29,12 +29,12 @@ %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true } %span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "✓" || !column.visible && " " }} %span{ :class => 'two columns omega' } {{column.name }} - .row - .sixteen.columns.alpha#loading{ 'ng-if' => 'shop_id && !loaded()' } - %img.spinner{ src: "/assets/spinning-circles.svg" } - %h1 LOADING CUSTOMERS - .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredCustomers.length == 0'} - %h1#no_results No customers found. + .row{ 'ng-if' => 'shop_id && !loaded()' } + .sixteen.columns.alpha#loading + %img.spinner{ src: "/assets/spinning-circles.svg" } + %h1 LOADING CUSTOMERS + .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredCustomers.length == 0'} + %h1#no_results No customers found. .row{ ng: { show: "loaded()" } } @@ -55,7 +55,8 @@ %td.bulk %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } %td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }} - %td.code{ 'ng-show' => 'columns.code.visible' } {{ customer.code }} + %td.code{ 'ng-show' => 'columns.code.visible' } + %input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" } %td.actions %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } - -# %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' } + %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' } diff --git a/spec/features/admin/customers_spec.rb b/spec/features/admin/customers_spec.rb index 38af3145f3..75088bf395 100644 --- a/spec/features/admin/customers_spec.rb +++ b/spec/features/admin/customers_spec.rb @@ -19,7 +19,7 @@ feature 'Customers' do visit admin_customers_path end - it "lists my customers", js: true do + it "passes the smoke test", js: true do # Prompts for a hub expect(page).to have_select2 "shop_id", with_options: [managed_distributor.name], without_options: [unmanaged_distributor.name] @@ -44,6 +44,15 @@ feature 'Customers' do first("div#columns_dropdown div.menu div.menu_item", text: "Email").click expect(page).to_not have_selector "th.email" expect(page).to_not have_content customer1.email + + # Updating attributes + within "tr#c_#{customer1.id}" do + fill_in "code", with: "new-customer-code" + expect(page).to have_css "input#code.update-pending" + end + click_button "Update" + expect(page).to have_css "input#code.update-success" + expect(customer1.reload.code).to eq "new-customer-code" end end end From 5b49e64bf204c7065db3adb91b3cddbc5a79fc1f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 12:52:36 +1000 Subject: [PATCH 27/56] Hiding bulk checkboxes on customer interface (for now) --- app/views/admin/customers/index.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index a8e6141796..c234705e4f 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -42,8 +42,8 @@ %table.index#customers{ :class => "sixteen columns alpha" } %thead %tr{ ng: { controller: "ColumnsCtrl" } } - %th.bulk - %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } + -# %th.bulk + -# %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } %th.email{ 'ng-show' => 'columns.email.visible' } %a{ :href => '', 'ng-click' => "predicate = 'customer.email'; reverse = !reverse" } Email %th.code{ 'ng-show' => 'columns.code.visible' } @@ -52,8 +52,8 @@ Ask?  %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" } - %td.bulk - %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } + -# %td.bulk + -# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } %td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }} %td.code{ 'ng-show' => 'columns.code.visible' } %input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" } From 4c5e75c3f96437d905e93562687538cccb2f9287 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 14:55:00 +1000 Subject: [PATCH 28/56] Adding acts-as-taggable-on gem --- Gemfile | 1 + Gemfile.lock | 3 ++ ...on_migration.acts_as_taggable_on_engine.rb | 31 +++++++++++++++++++ ...ique_indices.acts_as_taggable_on_engine.rb | 20 ++++++++++++ ...ache_to_tags.acts_as_taggable_on_engine.rb | 15 +++++++++ ...ggable_index.acts_as_taggable_on_engine.rb | 10 ++++++ ...or_tag_names.acts_as_taggable_on_engine.rb | 10 ++++++ db/schema.rb | 25 +++++++++++++-- 8 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20150508030520_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20150508030521_add_missing_unique_indices.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20150508030522_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20150508030523_add_missing_taggable_index.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20150508030524_change_collation_for_tag_names.acts_as_taggable_on_engine.rb diff --git a/Gemfile b/Gemfile index b63961a3ef..0393773df0 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,7 @@ gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg' gem 'angularjs-file-upload-rails', '~> 1.1.0' gem 'roadie-rails', '~> 1.0.3' gem 'figaro' +gem 'acts-as-taggable-on', '~> 3.4' gem 'foreigner' gem 'immigrant' diff --git a/Gemfile.lock b/Gemfile.lock index 1b62201120..3b1412d1c1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,6 +142,8 @@ GEM activesupport (3.2.21) i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) + acts-as-taggable-on (3.5.0) + activerecord (>= 3.2, < 5) acts_as_list (0.1.4) addressable (2.3.3) andand (1.3.3) @@ -532,6 +534,7 @@ PLATFORMS DEPENDENCIES active_model_serializers + acts-as-taggable-on (~> 3.4) andand angular-rails-templates angularjs-file-upload-rails (~> 1.1.0) diff --git a/db/migrate/20150508030520_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb b/db/migrate/20150508030520_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..6bbd5594ea --- /dev/null +++ b/db/migrate/20150508030520_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb @@ -0,0 +1,31 @@ +# This migration comes from acts_as_taggable_on_engine (originally 1) +class ActsAsTaggableOnMigration < ActiveRecord::Migration + def self.up + create_table :tags do |t| + t.string :name + end + + create_table :taggings do |t| + t.references :tag + + # You should make sure that the column created is + # long enough to store the required class names. + t.references :taggable, polymorphic: true + t.references :tagger, polymorphic: true + + # Limit is created to prevent MySQL error on index + # length for MyISAM table type: http://bit.ly/vgW2Ql + t.string :context, limit: 128 + + t.datetime :created_at + end + + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type, :context] + end + + def self.down + drop_table :taggings + drop_table :tags + end +end diff --git a/db/migrate/20150508030521_add_missing_unique_indices.acts_as_taggable_on_engine.rb b/db/migrate/20150508030521_add_missing_unique_indices.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..4ca676f6c7 --- /dev/null +++ b/db/migrate/20150508030521_add_missing_unique_indices.acts_as_taggable_on_engine.rb @@ -0,0 +1,20 @@ +# This migration comes from acts_as_taggable_on_engine (originally 2) +class AddMissingUniqueIndices < ActiveRecord::Migration + def self.up + add_index :tags, :name, unique: true + + remove_index :taggings, :tag_id + remove_index :taggings, [:taggable_id, :taggable_type, :context] + add_index :taggings, + [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type], + unique: true, name: 'taggings_idx' + end + + def self.down + remove_index :tags, :name + + remove_index :taggings, name: 'taggings_idx' + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type, :context] + end +end diff --git a/db/migrate/20150508030522_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb b/db/migrate/20150508030522_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..8edb508078 --- /dev/null +++ b/db/migrate/20150508030522_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb @@ -0,0 +1,15 @@ +# This migration comes from acts_as_taggable_on_engine (originally 3) +class AddTaggingsCounterCacheToTags < ActiveRecord::Migration + def self.up + add_column :tags, :taggings_count, :integer, default: 0 + + ActsAsTaggableOn::Tag.reset_column_information + ActsAsTaggableOn::Tag.find_each do |tag| + ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings) + end + end + + def self.down + remove_column :tags, :taggings_count + end +end diff --git a/db/migrate/20150508030523_add_missing_taggable_index.acts_as_taggable_on_engine.rb b/db/migrate/20150508030523_add_missing_taggable_index.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..71f2d7f433 --- /dev/null +++ b/db/migrate/20150508030523_add_missing_taggable_index.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# This migration comes from acts_as_taggable_on_engine (originally 4) +class AddMissingTaggableIndex < ActiveRecord::Migration + def self.up + add_index :taggings, [:taggable_id, :taggable_type, :context] + end + + def self.down + remove_index :taggings, [:taggable_id, :taggable_type, :context] + end +end diff --git a/db/migrate/20150508030524_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150508030524_change_collation_for_tag_names.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..bfb06bc7cd --- /dev/null +++ b/db/migrate/20150508030524_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# This migration comes from acts_as_taggable_on_engine (originally 5) +# This migration is added to circumvent issue #623 and have special characters +# work properly +class ChangeCollationForTagNames < ActiveRecord::Migration + def up + if ActsAsTaggableOn::Utils.using_mysql? + execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;") + end + end +end diff --git a/db/schema.rb b/db/schema.rb index cb8b503583..9f3cee5f5e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150424025907) do +ActiveRecord::Schema.define(:version => 20150508030524) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -549,6 +549,7 @@ ActiveRecord::Schema.define(:version => 20150424025907) do t.string "currency" t.decimal "distribution_fee", :precision => 10, :scale => 2 t.string "shipping_method_name" + t.decimal "unit_value", :precision => 8, :scale => 2 end add_index "spree_line_items", ["order_id"], :name => "index_line_items_on_order_id" @@ -618,9 +619,9 @@ ActiveRecord::Schema.define(:version => 20150424025907) do t.string "email" t.text "special_instructions" t.integer "distributor_id" + t.integer "order_cycle_id" t.string "currency" t.string "last_ip_address" - t.integer "order_cycle_id" t.integer "cart_id" end @@ -1080,6 +1081,26 @@ ActiveRecord::Schema.define(:version => 20150424025907) do t.integer "state_id" end + create_table "taggings", :force => true do |t| + t.integer "tag_id" + t.integer "taggable_id" + t.string "taggable_type" + t.integer "tagger_id" + t.string "tagger_type" + t.string "context", :limit => 128 + t.datetime "created_at" + end + + add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], :name => "taggings_idx", :unique => true + add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context" + + create_table "tags", :force => true do |t| + t.string "name" + t.integer "taggings_count", :default => 0 + end + + add_index "tags", ["name"], :name => "index_tags_on_name", :unique => true + create_table "variant_overrides", :force => true do |t| t.integer "variant_id", :null => false t.integer "hub_id", :null => false From a60fd3d388bcb34780e08873a203dbcdda2223b2 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 14:56:03 +1000 Subject: [PATCH 29/56] Installing ngTagInput --- app/assets/javascripts/shared/ng-tags-input.min.js | 1 + app/assets/stylesheets/shared/ng-tags-input.min.css | 1 + 2 files changed, 2 insertions(+) create mode 100755 app/assets/javascripts/shared/ng-tags-input.min.js create mode 100755 app/assets/stylesheets/shared/ng-tags-input.min.css diff --git a/app/assets/javascripts/shared/ng-tags-input.min.js b/app/assets/javascripts/shared/ng-tags-input.min.js new file mode 100755 index 0000000000..9a1acd6e0d --- /dev/null +++ b/app/assets/javascripts/shared/ng-tags-input.min.js @@ -0,0 +1 @@ +/*! ngTagsInput v2.3.0 License: MIT */!function(){"use strict";var a={backspace:8,tab:9,enter:13,escape:27,space:32,up:38,down:40,left:37,right:39,"delete":46,comma:188},b=9007199254740991,c=["text","email","url"],d=angular.module("ngTagsInput",[]);d.directive("tagsInput",["$timeout","$document","$window","tagsInputConfig","tiUtil",function(d,e,f,g,h){function i(a,b,c,d){var e,f,g,i={};return e=function(b){return h.safeToString(b[a.displayProperty])},f=function(b,c){b[a.displayProperty]=c},g=function(b){var d=e(b);return d&&d.length>=a.minLength&&d.length<=a.maxLength&&a.allowedTagsPattern.test(d)&&!h.findInObjectArray(i.items,b,a.keyProperty||a.displayProperty)&&c({$tag:b})},i.items=[],i.addText=function(a){var b={};return f(b,a),i.add(b)},i.add=function(c){var d=e(c);return a.replaceSpacesWithDashes&&(d=h.replaceSpacesWithDashes(d)),f(c,d),g(c)?(i.items.push(c),b.trigger("tag-added",{$tag:c})):d&&b.trigger("invalid-tag",{$tag:c}),c},i.remove=function(a){var c=i.items[a];return d({$tag:c})?(i.items.splice(a,1),i.clearSelection(),b.trigger("tag-removed",{$tag:c}),c):void 0},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a]},i.selectPrior=function(){i.select(--i.index)},i.selectNext=function(){i.select(++i.index)},i.removeSelected=function(){return i.remove(i.index)},i.clearSelection=function(){i.selected=null,i.index=-1},i.clearSelection(),i}function j(a){return-1!==c.indexOf(a)}return{restrict:"E",require:"ngModel",scope:{tags:"=ngModel",onTagAdding:"&",onTagAdded:"&",onInvalidTag:"&",onTagRemoving:"&",onTagRemoved:"&"},replace:!1,transclude:!0,templateUrl:"ngTagsInput/tags-input.html",controller:["$scope","$attrs","$element",function(a,c,d){a.events=h.simplePubSub(),g.load("tagsInput",a,c,{template:[String,"ngTagsInput/tag-item.html"],type:[String,"text",j],placeholder:[String,"Add a tag"],tabindex:[Number,null],removeTagSymbol:[String,String.fromCharCode(215)],replaceSpacesWithDashes:[Boolean,!0],minLength:[Number,3],maxLength:[Number,b],addOnEnter:[Boolean,!0],addOnSpace:[Boolean,!1],addOnComma:[Boolean,!0],addOnBlur:[Boolean,!0],addOnPaste:[Boolean,!1],pasteSplitPattern:[RegExp,/,/],allowedTagsPattern:[RegExp,/.+/],enableEditingLastTag:[Boolean,!1],minTags:[Number,0],maxTags:[Number,b],displayProperty:[String,"text"],keyProperty:[String,""],allowLeftoverText:[Boolean,!1],addFromAutocompleteOnly:[Boolean,!1],spellcheck:[Boolean,!0]}),a.tagList=new i(a.options,a.events,h.handleUndefinedResult(a.onTagAdding,!0),h.handleUndefinedResult(a.onTagRemoving,!0)),this.registerAutocomplete=function(){var b=d.find("input");return{addTag:function(b){return a.tagList.add(b)},focusInput:function(){b[0].focus()},getTags:function(){return a.tags},getCurrentTagText:function(){return a.newTag.text},getOptions:function(){return a.options},on:function(b,c){return a.events.on(b,c),this}}},this.registerTagItem=function(){return{getOptions:function(){return a.options},removeTag:function(b){a.disabled||a.tagList.remove(b)}}}}],link:function(b,c,g,i){var j,k=[a.enter,a.comma,a.space,a.backspace,a["delete"],a.left,a.right],l=b.tagList,m=b.events,n=b.options,o=c.find("input"),p=["minTags","maxTags","allowLeftoverText"];j=function(){i.$setValidity("maxTags",b.tags.length<=n.maxTags),i.$setValidity("minTags",b.tags.length>=n.minTags),i.$setValidity("leftoverText",b.hasFocus||n.allowLeftoverText?!0:!b.newTag.text)},i.$isEmpty=function(a){return!a||!a.length},b.newTag={text:"",invalid:null,setText:function(a){this.text=a,m.trigger("input-change",a)}},b.track=function(a){return a[n.keyProperty||n.displayProperty]},b.$watch("tags",function(a){b.tags=h.makeObjectArray(a,n.displayProperty),l.items=b.tags}),b.$watch("tags.length",function(){j()}),g.$observe("disabled",function(a){b.disabled=a}),b.eventHandlers={input:{change:function(a){m.trigger("input-change",a)},keydown:function(a){m.trigger("input-keydown",a)},focus:function(){b.hasFocus||(b.hasFocus=!0,m.trigger("input-focus"))},blur:function(){d(function(){var a=e.prop("activeElement"),d=a===o[0],f=c[0].contains(a);(d||!f)&&(b.hasFocus=!1,m.trigger("input-blur"))})},paste:function(a){a.getTextData=function(){var b=a.clipboardData||a.originalEvent&&a.originalEvent.clipboardData;return b?b.getData("text/plain"):f.clipboardData.getData("Text")},m.trigger("input-paste",a)}},host:{click:function(){b.disabled||o[0].focus()}}},m.on("tag-added",b.onTagAdded).on("invalid-tag",b.onInvalidTag).on("tag-removed",b.onTagRemoved).on("tag-added",function(){b.newTag.setText("")}).on("tag-added tag-removed",function(){i.$setViewValue(b.tags)}).on("invalid-tag",function(){b.newTag.invalid=!0}).on("option-change",function(a){-1!==p.indexOf(a.name)&&j()}).on("input-change",function(){l.clearSelection(),b.newTag.invalid=null}).on("input-focus",function(){c.triggerHandler("focus"),i.$setValidity("leftoverText",!0)}).on("input-blur",function(){n.addOnBlur&&!n.addFromAutocompleteOnly&&l.addText(b.newTag.text),c.triggerHandler("blur"),j()}).on("input-keydown",function(c){var d,e,f,g,h=c.keyCode,i=c.shiftKey||c.altKey||c.ctrlKey||c.metaKey,j={};if(!i&&-1!==k.indexOf(h)){if(j[a.enter]=n.addOnEnter,j[a.comma]=n.addOnComma,j[a.space]=n.addOnSpace,d=!n.addFromAutocompleteOnly&&j[h],e=(h===a.backspace||h===a["delete"])&&l.selected,g=h===a.backspace&&0===b.newTag.text.length&&n.enableEditingLastTag,f=(h===a.backspace||h===a.left||h===a.right)&&0===b.newTag.text.length&&!n.enableEditingLastTag,d)l.addText(b.newTag.text);else if(g){var m;l.selectPrior(),m=l.removeSelected(),m&&b.newTag.setText(m[n.displayProperty])}else e?l.removeSelected():f&&(h===a.left||h===a.backspace?l.selectPrior():h===a.right&&l.selectNext());(d||f||e||g)&&c.preventDefault()}}).on("input-paste",function(a){if(n.addOnPaste){var b=a.getTextData(),c=b.split(n.pasteSplitPattern);c.length>1&&(c.forEach(function(a){l.addText(a)}),a.preventDefault())}})}}}]),d.directive("tiTagItem",["tiUtil",function(a){return{restrict:"E",require:"^tagsInput",template:'',scope:{data:"="},link:function(b,c,d,e){var f=e.registerTagItem(),g=f.getOptions();b.$$template=g.template,b.$$removeTagSymbol=g.removeTagSymbol,b.$getDisplayText=function(){return a.safeToString(b.data[g.displayProperty])},b.$removeTag=function(){f.removeTag(b.$index)},b.$watch("$parent.$index",function(a){b.$index=a})}}}]),d.directive("autoComplete",["$document","$timeout","$sce","$q","tagsInputConfig","tiUtil",function(b,c,d,e,f,g){function h(a,b,c){var d,f,h,i={};return h=function(){return b.tagsInput.keyProperty||b.tagsInput.displayProperty},d=function(a,c){return a.filter(function(a){return!g.findInObjectArray(c,a,h(),function(a,c){return b.tagsInput.replaceSpacesWithDashes&&(a=g.replaceSpacesWithDashes(a),c=g.replaceSpacesWithDashes(c)),g.defaultComparer(a,c)})})},i.reset=function(){f=null,i.items=[],i.visible=!1,i.index=-1,i.selected=null,i.query=null},i.show=function(){b.selectFirstMatch?i.select(0):i.selected=null,i.visible=!0},i.load=g.debounce(function(c,j){i.query=c;var k=e.when(a({$query:c}));f=k,k.then(function(a){k===f&&(a=g.makeObjectArray(a.data||a,h()),a=d(a,j),i.items=a.slice(0,b.maxResultsToShow),i.items.length>0?i.show():i.reset())})},b.debounceDelay),i.selectNext=function(){i.select(++i.index)},i.selectPrior=function(){i.select(--i.index)},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a],c.trigger("suggestion-selected",a)},i.reset(),i}function i(a,b){var c=a.find("li").eq(b),d=c.parent(),e=c.prop("offsetTop"),f=c.prop("offsetHeight"),g=d.prop("clientHeight"),h=d.prop("scrollTop");h>e?d.prop("scrollTop",e):e+f>g+h&&d.prop("scrollTop",e+f-g)}return{restrict:"E",require:"^tagsInput",scope:{source:"&"},templateUrl:"ngTagsInput/auto-complete.html",controller:["$scope","$element","$attrs",function(a,b,c){a.events=g.simplePubSub(),f.load("autoComplete",a,c,{template:[String,"ngTagsInput/auto-complete-match.html"],debounceDelay:[Number,100],minLength:[Number,3],highlightMatchedText:[Boolean,!0],maxResultsToShow:[Number,10],loadOnDownArrow:[Boolean,!1],loadOnEmpty:[Boolean,!1],loadOnFocus:[Boolean,!1],selectFirstMatch:[Boolean,!0],displayProperty:[String,""]}),a.suggestionList=new h(a.source,a.options,a.events),this.registerAutocompleteMatch=function(){return{getOptions:function(){return a.options},getQuery:function(){return a.suggestionList.query}}}}],link:function(b,c,d,e){var f,g=[a.enter,a.tab,a.escape,a.up,a.down],h=b.suggestionList,j=e.registerAutocomplete(),k=b.options,l=b.events;k.tagsInput=j.getOptions(),f=function(a){return a&&a.length>=k.minLength||!a&&k.loadOnEmpty},b.addSuggestionByIndex=function(a){h.select(a),b.addSuggestion()},b.addSuggestion=function(){var a=!1;return h.selected&&(j.addTag(angular.copy(h.selected)),h.reset(),j.focusInput(),a=!0),a},b.track=function(a){return a[k.tagsInput.keyProperty||k.tagsInput.displayProperty]},j.on("tag-added invalid-tag input-blur",function(){h.reset()}).on("input-change",function(a){f(a)?h.load(a,j.getTags()):h.reset()}).on("input-focus",function(){var a=j.getCurrentTagText();k.loadOnFocus&&f(a)&&h.load(a,j.getTags())}).on("input-keydown",function(c){var d=c.keyCode,e=!1;if(-1!==g.indexOf(d))return h.visible?d===a.down?(h.selectNext(),e=!0):d===a.up?(h.selectPrior(),e=!0):d===a.escape?(h.reset(),e=!0):(d===a.enter||d===a.tab)&&(e=b.addSuggestion()):d===a.down&&b.options.loadOnDownArrow&&(h.load(j.getCurrentTagText(),j.getTags()),e=!0),e?(c.preventDefault(),c.stopImmediatePropagation(),!1):void 0}),l.on("suggestion-selected",function(a){i(c,a)})}}}]),d.directive("tiAutocompleteMatch",["$sce","tiUtil",function(a,b){return{restrict:"E",require:"^autoComplete",template:'',scope:{data:"="},link:function(c,d,e,f){var g=f.registerAutocompleteMatch(),h=g.getOptions();c.$$template=h.template,c.$index=c.$parent.$index,c.$highlight=function(c){return h.highlightMatchedText&&(c=b.safeHighlight(c,g.getQuery())),a.trustAsHtml(c)},c.$getDisplayText=function(){return b.safeToString(c.data[h.displayProperty||h.tagsInput.displayProperty])}}}}]),d.directive("tiTranscludeAppend",function(){return function(a,b,c,d,e){e(function(a){b.append(a)})}}),d.directive("tiAutosize",["tagsInputConfig",function(a){return{restrict:"A",require:"ngModel",link:function(b,c,d,e){var f,g,h=a.getTextAutosizeThreshold();f=angular.element(''),f.css("display","none").css("visibility","hidden").css("width","auto").css("white-space","pre"),c.parent().append(f),g=function(a){var b,e=a;return angular.isString(e)&&0===e.length&&(e=d.placeholder),e&&(f.text(e),f.css("display",""),b=f.prop("offsetWidth"),f.css("display","none")),c.css("width",b?b+h+"px":""),a},e.$parsers.unshift(g),e.$formatters.unshift(g),d.$observe("placeholder",function(a){e.$modelValue||g(a)})}}}]),d.directive("tiBindAttrs",function(){return function(a,b,c){a.$watch(c.tiBindAttrs,function(a){angular.forEach(a,function(a,b){c.$set(b,a)})},!0)}}),d.provider("tagsInputConfig",function(){var a={},b={},c=3;this.setDefaults=function(b,c){return a[b]=c,this},this.setActiveInterpolation=function(a,c){return b[a]=c,this},this.setTextAutosizeThreshold=function(a){return c=a,this},this.$get=["$interpolate",function(d){var e={};return e[String]=function(a){return a},e[Number]=function(a){return parseInt(a,10)},e[Boolean]=function(a){return"true"===a.toLowerCase()},e[RegExp]=function(a){return new RegExp(a)},{load:function(c,f,g,h){var i=function(){return!0};f.options={},angular.forEach(h,function(h,j){var k,l,m,n,o,p;k=h[0],l=h[1],m=h[2]||i,n=e[k],o=function(){var b=a[c]&&a[c][j];return angular.isDefined(b)?b:l},p=function(a){f.options[j]=a&&m(a)?n(a):o()},b[c]&&b[c][j]?g.$observe(j,function(a){p(a),f.events.trigger("option-change",{name:j,newValue:a})}):p(g[j]&&d(g[j])(f.$parent))})},getTextAutosizeThreshold:function(){return c}}}]}),d.factory("tiUtil",["$timeout",function(a){var b={};return b.debounce=function(b,c){var d;return function(){var e=arguments;a.cancel(d),d=a(function(){b.apply(null,e)},c)}},b.makeObjectArray=function(a,b){return a=a||[],a.length>0&&!angular.isObject(a[0])&&a.forEach(function(c,d){a[d]={},a[d][b]=c}),a},b.findInObjectArray=function(a,c,d,e){var f=null;return e=e||b.defaultComparer,a.some(function(a){return e(a[d],c[d])?(f=a,!0):void 0}),f},b.defaultComparer=function(a,c){return b.safeToString(a).toLowerCase()===b.safeToString(c).toLowerCase()},b.safeHighlight=function(a,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}if(!c)return a;a=b.encodeHTML(a),c=b.encodeHTML(c);var e=new RegExp("&[^;]+;|"+d(c),"gi");return a.replace(e,function(a){return a.toLowerCase()===c.toLowerCase()?""+a+"":a})},b.safeToString=function(a){return angular.isUndefined(a)||null==a?"":a.toString().trim()},b.encodeHTML=function(a){return b.safeToString(a).replace(/&/g,"&").replace(//g,">")},b.handleUndefinedResult=function(a,b){return function(){var c=a.apply(null,arguments);return angular.isUndefined(c)?b:c}},b.replaceSpacesWithDashes=function(a){return b.safeToString(a).replace(/\s/g,"-")},b.simplePubSub=function(){var a={};return{on:function(b,c){return b.split(" ").forEach(function(b){a[b]||(a[b]=[]),a[b].push(c)}),this},trigger:function(c,d){var e=a[c]||[];return e.every(function(a){return b.handleUndefinedResult(a,!0)(d)}),this}}},b}]),d.run(["$templateCache",function(a){a.put("ngTagsInput/tags-input.html",'
'),a.put("ngTagsInput/tag-item.html",' '),a.put("ngTagsInput/auto-complete.html",'
'),a.put("ngTagsInput/auto-complete-match.html",'')}])}(); \ No newline at end of file diff --git a/app/assets/stylesheets/shared/ng-tags-input.min.css b/app/assets/stylesheets/shared/ng-tags-input.min.css new file mode 100755 index 0000000000..ee4a4a98d5 --- /dev/null +++ b/app/assets/stylesheets/shared/ng-tags-input.min.css @@ -0,0 +1 @@ +tags-input{display:block}tags-input *,tags-input :after,tags-input :before{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}tags-input .host{position:relative;margin-top:5px;margin-bottom:5px;height:100%}tags-input .host:active{outline:0}tags-input .tags{-moz-appearance:textfield;-webkit-appearance:textfield;padding:1px;overflow:hidden;word-wrap:break-word;cursor:text;background-color:#fff;border:1px solid #a9a9a9;box-shadow:1px 1px 1px 0 #d3d3d3 inset;height:100%}tags-input .tags.focused{outline:0;-webkit-box-shadow:0 0 3px 1px rgba(5,139,242,.6);-moz-box-shadow:0 0 3px 1px rgba(5,139,242,.6);box-shadow:0 0 3px 1px rgba(5,139,242,.6)}tags-input .tags .tag-list{margin:0;padding:0;list-style-type:none}tags-input .tags .tag-item{margin:2px;padding:0 5px;display:inline-block;float:left;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif;height:26px;line-height:25px;border:1px solid #acacac;border-radius:3px;background:-webkit-linear-gradient(top,#f0f9ff 0,#cbebff 47%,#a1dbff 100%);background:linear-gradient(to bottom,#f0f9ff 0,#cbebff 47%,#a1dbff 100%)}tags-input .tags .tag-item.selected{background:-webkit-linear-gradient(top,#febbbb 0,#fe9090 45%,#ff5c5c 100%);background:linear-gradient(to bottom,#febbbb 0,#fe9090 45%,#ff5c5c 100%)}tags-input .tags .tag-item .remove-button{margin:0 0 0 5px;padding:0;border:none;background:0 0;cursor:pointer;vertical-align:middle;font:700 16px Arial,sans-serif;color:#585858}tags-input .tags .tag-item .remove-button:active{color:red}tags-input .tags .input{border:0;outline:0;margin:2px;padding:0;padding-left:5px;float:left;height:26px;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif}tags-input .tags .input.invalid-tag{color:red}tags-input .tags .input::-ms-clear{display:none}tags-input.ng-invalid .tags{-webkit-box-shadow:0 0 3px 1px rgba(255,0,0,.6);-moz-box-shadow:0 0 3px 1px rgba(255,0,0,.6);box-shadow:0 0 3px 1px rgba(255,0,0,.6)}tags-input[disabled] .host:focus{outline:0}tags-input[disabled] .tags{background-color:#eee;cursor:default}tags-input[disabled] .tags .tag-item{opacity:.65;background:-webkit-linear-gradient(top,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%);background:linear-gradient(to bottom,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%)}tags-input[disabled] .tags .tag-item .remove-button{cursor:default}tags-input[disabled] .tags .tag-item .remove-button:active{color:#585858}tags-input[disabled] .tags .input{background-color:#eee;cursor:default}tags-input .autocomplete{margin-top:5px;position:absolute;padding:5px 0;z-index:999;width:100%;background-color:#fff;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}tags-input .autocomplete .suggestion-list{margin:0;padding:0;list-style-type:none;max-height:280px;overflow-y:auto;position:relative}tags-input .autocomplete .suggestion-item{padding:5px 10px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font:16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff}tags-input .autocomplete .suggestion-item.selected,tags-input .autocomplete .suggestion-item.selected em{color:#fff;background-color:#0097cf}tags-input .autocomplete .suggestion-item em{font:normal bold 16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff} \ No newline at end of file From a03c7a5f220a3cac71b0a4910e2845550e9fb7e7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 14:56:57 +1000 Subject: [PATCH 30/56] Adding ngTagsInput to admin assets --- app/assets/javascripts/admin/all.js | 1 + app/assets/stylesheets/admin/all.css | 1 + 2 files changed, 2 insertions(+) diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index f6d016dc9a..e44e6ab96d 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -17,6 +17,7 @@ //= require admin/spree_promo //= require admin/spree_paypal_express //= require ../shared/ng-infinite-scroll.min.js +//= require ../shared/ng-tags-input.min.js //= require ./admin //= require ./customers/customers //= require ./dropdown/dropdown diff --git a/app/assets/stylesheets/admin/all.css b/app/assets/stylesheets/admin/all.css index 9b3603fe7b..e0d668b95b 100644 --- a/app/assets/stylesheets/admin/all.css +++ b/app/assets/stylesheets/admin/all.css @@ -10,6 +10,7 @@ *= require shared/jquery-ui-timepicker-addon *= require shared/textAngular.min + *= require shared/ng-tags-input.min *= require_self *= require_tree . From 5b3c9842e4e0b39ccdd297975c4ed2b6d8afdd70 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 14:58:56 +1000 Subject: [PATCH 31/56] Adding acts_as_taggable to customer model --- app/models/customer.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/customer.rb b/app/models/customer.rb index d3fa9e093f..b17e1407fa 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -1,4 +1,6 @@ class Customer < ActiveRecord::Base + acts_as_taggable + belongs_to :enterprise belongs_to :user, :class_name => Spree.user_class From d2e8b23dd4467ac3ab3c7833c20b5dd8b6230668 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 14:59:16 +1000 Subject: [PATCH 32/56] Creating association between enterprise and customers --- app/models/enterprise.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 1aa76b88a8..c11bfb97b4 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -30,6 +30,7 @@ class Enterprise < ActiveRecord::Base has_and_belongs_to_many :payment_methods, join_table: 'distributors_payment_methods', class_name: 'Spree::PaymentMethod', foreign_key: 'distributor_id' has_many :distributor_shipping_methods, foreign_key: :distributor_id has_many :shipping_methods, through: :distributor_shipping_methods + has_many :customers delegate :latitude, :longitude, :city, :state_name, :to => :address From 3bc69242ce9e0f7fc889e505f3aa72fae7d6f20a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 15:00:06 +1000 Subject: [PATCH 33/56] Style tweaks --- app/assets/stylesheets/admin/orders.css.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index c0c1dac86b..249447757c 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -8,15 +8,15 @@ } } -input.update-pending { +.update-pending { border: solid 1px orange; } -input.update-error { +.update-error { border: solid 1px red; } -input.update-success { +.update-success { border: solid 1px #9fc820; } @@ -42,4 +42,4 @@ div#group_buy_calculation { .row span { text-align: center; } -} \ No newline at end of file +} From b364994cc8caeb92664360c1118416c91aff2b0e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 15:30:04 +1000 Subject: [PATCH 34/56] Adding tags input to customer index using ngTagsInput --- .../customers_controller.js.coffee | 1 + .../admin/customers/customers.js.coffee | 2 +- .../tags_with_translation.js.coffee | 8 ++++++ .../api/admin/customer_serializer.rb | 10 ++++++- app/views/admin/customers/index.html.haml | 26 ++++++++++++------- spec/features/admin/customers_spec.rb | 18 +++++++++++-- 6 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index 5f078525d9..0e82806ef9 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -6,6 +6,7 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, Customers $scope.columns = Columns.setColumns email: { name: "Email", visible: true } code: { name: "Code", visible: true } + tags: { name: "Tags", visible: true } $scope.initialise = -> $scope.customers = Customers.index(enterprise_id: $scope.shop.id) diff --git a/app/assets/javascripts/admin/customers/customers.js.coffee b/app/assets/javascripts/admin/customers/customers.js.coffee index 462b7d5818..3733fe2eea 100644 --- a/app/assets/javascripts/admin/customers/customers.js.coffee +++ b/app/assets/javascripts/admin/customers/customers.js.coffee @@ -1 +1 @@ -angular.module("admin.customers", ['ngResource', 'admin.indexUtils', 'admin.dropdown']) \ No newline at end of file +angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.dropdown']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee new file mode 100644 index 0000000000..e15ec10342 --- /dev/null +++ b/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.customers").directive "tagsWithTranslation", -> + restrict: "E" + template: "" + scope: + object: "=" + link: (scope, element, attrs) -> + scope.$watchCollection "object.tags", -> + scope.object.tag_list = (tag.text for tag in scope.object.tags).join(",") diff --git a/app/serializers/api/admin/customer_serializer.rb b/app/serializers/api/admin/customer_serializer.rb index 84f32b6e8b..3cb9518a9f 100644 --- a/app/serializers/api/admin/customer_serializer.rb +++ b/app/serializers/api/admin/customer_serializer.rb @@ -1,3 +1,11 @@ class Api::Admin::CustomerSerializer < ActiveModel::Serializer - attributes :id, :email, :enterprise_id, :user_id, :code + attributes :id, :email, :enterprise_id, :user_id, :code, :tags, :tag_list + + def tag_list + object.tag_list.join(",") + end + + def tags + object.tag_list.map{ |t| { text: t } } + end end diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index c234705e4f..603f929477 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -39,7 +39,11 @@ .row{ ng: { show: "loaded()" } } %form{ name: "customers" } - %table.index#customers{ :class => "sixteen columns alpha" } + %table.index#customers + %col.email{ width: "20%"} + %col.code{ width: "20%"} + %col.tags{ width: "50%"} + %col.actions{ width: "10%"} %thead %tr{ ng: { controller: "ColumnsCtrl" } } -# %th.bulk @@ -48,15 +52,19 @@ %a{ :href => '', 'ng-click' => "predicate = 'customer.email'; reverse = !reverse" } Email %th.code{ 'ng-show' => 'columns.code.visible' } %a{ :href => '', 'ng-click' => "predicate = 'customer.code'; reverse = !reverse" } Code + %th.tags{ 'ng-show' => 'columns.tags.visible' } Tags %th.actions Ask?  %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } - %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" } - -# %td.bulk - -# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } - %td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }} - %td.code{ 'ng-show' => 'columns.code.visible' } - %input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" } - %td.actions - %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } + %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" } + -# %td.bulk + -# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } + %td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }} + %td.code{ 'ng-show' => 'columns.code.visible' } + %input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" } + %td.tags{ 'ng-show' => 'columns.tags.visible' } + .tag_watcher{ 'obj-for-update' => "customer", "attr-for-update" => "tag_list"} + %tags_with_translation{ object: 'customer' } + %td.actions + %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' } diff --git a/spec/features/admin/customers_spec.rb b/spec/features/admin/customers_spec.rb index 75088bf395..b6c84e10cd 100644 --- a/spec/features/admin/customers_spec.rb +++ b/spec/features/admin/customers_spec.rb @@ -20,7 +20,7 @@ feature 'Customers' do end it "passes the smoke test", js: true do - # Prompts for a hub + # Prompts for a hub for a list of my managed enterprises expect(page).to have_select2 "shop_id", with_options: [managed_distributor.name], without_options: [unmanaged_distributor.name] select2_select managed_distributor.name, from: "shop_id" @@ -44,15 +44,29 @@ feature 'Customers' do first("div#columns_dropdown div.menu div.menu_item", text: "Email").click expect(page).to_not have_selector "th.email" expect(page).to_not have_content customer1.email + end + + it "allows updating of attributes", js: true do + select2_select managed_distributor.name, from: "shop_id" + click_button "Go" - # Updating attributes within "tr#c_#{customer1.id}" do fill_in "code", with: "new-customer-code" expect(page).to have_css "input#code.update-pending" end + within "tr#c_#{customer1.id}" do + find(:css, "tags-input .tags input").set "awesome\n" + expect(page).to have_css ".tag_watcher.update-pending" + end click_button "Update" + + # Every says it updated expect(page).to have_css "input#code.update-success" + expect(page).to have_css ".tag_watcher.update-success" + + # And it actually did expect(customer1.reload.code).to eq "new-customer-code" + expect(customer1.tag_list).to eq ["awesome"] end end end From dd9c192d480381dca6c9098002035f4ee0f04380 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 16:13:57 +1000 Subject: [PATCH 35/56] Shuffling layout a little on customer index --- app/views/admin/customers/index.html.haml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index 603f929477..f3e82f9cdf 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -1,3 +1,6 @@ +- content_for :page_title do + %h1.page-title Customers + = admin_inject_shops %div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } } @@ -11,16 +14,16 @@ .row{ 'ng-hide' => '!loaded() || lineItems.length == 0' } .controls{ :class => "sixteen columns alpha", :style => "margin-bottom: 15px;" } - .three.columns.alpha + .five.columns.alpha %input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } - .three.columns   + .five.columns   -# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true } -# %span{ :class => 'icon-check' }   Actions -# %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } -# %div.menu{ 'ng-show' => "expanded" } -# %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "action in bulkActions", 'ng-click' => "selectedBulkAction.callback(filteredCustomers)", 'ofn-close-on-click' => true } -# %span{ :class => 'three columns omega' } {{action.name }} - .seven.columns   + .three.columns   .three.columns.omega %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' } %span{ :class => 'icon-reorder' }   Columns From ed941e211d771526c06cd1e50878311c25c83978 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 8 May 2015 17:35:58 +1000 Subject: [PATCH 36/56] Don't require a code on customer --- app/models/customer.rb | 4 ++-- ...508072454_remove_customer_code_not_null_constraint.rb | 9 +++++++++ db/schema.rb | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20150508072454_remove_customer_code_not_null_constraint.rb diff --git a/app/models/customer.rb b/app/models/customer.rb index b17e1407fa..6bbd4606e5 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -4,8 +4,8 @@ class Customer < ActiveRecord::Base belongs_to :enterprise belongs_to :user, :class_name => Spree.user_class - validates :code, presence: true, uniqueness: {scope: :enterprise_id} - validates :email, presence: true + validates :code, uniqueness: { scope: :enterprise_id, allow_blank: true, allow_nil: true } + validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: "is associated with an existing customer" } validates :enterprise_id, presence: true scope :of, ->(enterprise) { where(enterprise_id: enterprise) } diff --git a/db/migrate/20150508072454_remove_customer_code_not_null_constraint.rb b/db/migrate/20150508072454_remove_customer_code_not_null_constraint.rb new file mode 100644 index 0000000000..deeacbc608 --- /dev/null +++ b/db/migrate/20150508072454_remove_customer_code_not_null_constraint.rb @@ -0,0 +1,9 @@ +class RemoveCustomerCodeNotNullConstraint < ActiveRecord::Migration + def up + change_column :customers, :code, :string, null: true + end + + def down + change_column :customers, :code, :string, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 9f3cee5f5e..e5692233d3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150508030524) do +ActiveRecord::Schema.define(:version => 20150508072454) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -158,7 +158,7 @@ ActiveRecord::Schema.define(:version => 20150508030524) do create_table "customers", :force => true do |t| t.string "email", :null => false t.integer "enterprise_id", :null => false - t.string "code", :null => false + t.string "code" t.integer "user_id" t.datetime "created_at", :null => false t.datetime "updated_at", :null => false From 1559b4e30abef454c0e42ad6f10724cb2aabe93a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 13 May 2015 11:39:17 +1000 Subject: [PATCH 37/56] Adding customer reference to orders --- .../20150508072938_add_customer_to_orders.rb | 16 ++++++++++++++++ db/schema.rb | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20150508072938_add_customer_to_orders.rb diff --git a/db/migrate/20150508072938_add_customer_to_orders.rb b/db/migrate/20150508072938_add_customer_to_orders.rb new file mode 100644 index 0000000000..79d69baf49 --- /dev/null +++ b/db/migrate/20150508072938_add_customer_to_orders.rb @@ -0,0 +1,16 @@ +class AddCustomerToOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :customer_id, :integer + add_index :spree_orders, :customer_id + add_foreign_key :spree_orders, :customers, column: :customer_id + + Spree::Order.where("spree_orders.email IS NOT NULL AND distributor_id IS NOT NULL AND customer_id IS NULL").each do |order| + customer = Customer.find_by_email_and_enterprise_id(order.email, order.distributor_id) + unless customer.present? + user = Spree::User.find_by_email(order.email) + customer = Customer.create!(email: order.email, enterprise_id: order.distributor_id, user_id: user.andand.id ) + end + order.update_attribute(:customer, customer) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e5692233d3..b32ec3c4d7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150508072454) do +ActiveRecord::Schema.define(:version => 20150508072938) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -623,8 +623,10 @@ ActiveRecord::Schema.define(:version => 20150508072454) do t.string "currency" t.string "last_ip_address" t.integer "cart_id" + t.integer "customer_id" end + add_index "spree_orders", ["customer_id"], :name => "index_spree_orders_on_customer_id" add_index "spree_orders", ["number"], :name => "index_orders_on_number" create_table "spree_payment_methods", :force => true do |t| @@ -1206,6 +1208,7 @@ ActiveRecord::Schema.define(:version => 20150508072454) do add_foreign_key "spree_option_values_variants", "spree_variants", name: "spree_option_values_variants_variant_id_fk", column: "variant_id" add_foreign_key "spree_orders", "carts", name: "spree_orders_cart_id_fk" + add_foreign_key "spree_orders", "customers", name: "spree_orders_customer_id_fk" add_foreign_key "spree_orders", "enterprises", name: "spree_orders_distributor_id_fk", column: "distributor_id" add_foreign_key "spree_orders", "order_cycles", name: "spree_orders_order_cycle_id_fk" add_foreign_key "spree_orders", "spree_addresses", name: "spree_orders_bill_address_id_fk", column: "bill_address_id" From 34f5cfb6b5094b2d6169e230b051b87dafe16432 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 13 May 2015 12:32:55 +1000 Subject: [PATCH 38/56] Completed orders require a customer, add association logic --- app/models/customer.rb | 8 ++++++ app/models/spree/order_decorator.rb | 23 +++++++++++++++++ spec/models/customer_spec.rb | 20 +++++++++++++++ spec/models/spree/order_spec.rb | 39 +++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 spec/models/customer_spec.rb diff --git a/app/models/customer.rb b/app/models/customer.rb index 6bbd4606e5..856f7e94d7 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -9,4 +9,12 @@ class Customer < ActiveRecord::Base validates :enterprise_id, presence: true scope :of, ->(enterprise) { where(enterprise_id: enterprise) } + + before_create :associate_user + + private + + def associate_user + self.user = user || Spree::User.find_by_email(email) + end end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 0dc6977965..28a8007da6 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -10,11 +10,14 @@ Spree::Order.class_eval do belongs_to :order_cycle belongs_to :distributor, :class_name => 'Enterprise' belongs_to :cart + belongs_to :customer + validates :customer, presence: true, if: :require_customer? validate :products_available_from_new_distribution, :if => lambda { distributor_id_changed? || order_cycle_id_changed? } attr_accessible :order_cycle_id, :distributor_id before_validation :shipping_address_from_distributor + before_validation :associate_customer, unless: :customer_is_valid? checkout_flow do go_to_state :address @@ -260,4 +263,24 @@ Spree::Order.class_eval do def product_distribution_for(line_item) line_item.variant.product.product_distribution_for self.distributor end + + def require_customer? + return true unless new_record? or state == 'cart' + end + + def customer_is_valid? + return true unless require_customer? + customer.present? && customer.enterprise_id == distributor_id && customer.email == (user.andand.email || email) + end + + def associate_customer + email_for_customer = user.andand.email || email + existing_customer = Customer.of(distributor).find_by_email(email_for_customer) + if existing_customer + self.customer = existing_customer + else + new_customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user) + self.customer = new_customer + end + end end diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb new file mode 100644 index 0000000000..0e43ce9df5 --- /dev/null +++ b/spec/models/customer_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Customer, type: :model do + describe 'creation callbacks' do + let!(:user1) { create(:user) } + let!(:user2) { create(:user) } + let!(:enterprise) { create(:distributor_enterprise) } + + it "associates an existing user using email" do + c1 = Customer.create(enterprise: enterprise, email: 'some-email-not-associated-with-a-user@email.com') + expect(c1.user).to be_nil + + c2 = Customer.create(enterprise: enterprise, email: 'some-email-not-associated-with-a-user@email.com', user: user1) + expect(c2.user).to eq user1 + + c3 = Customer.create(enterprise: enterprise, email: user2.email) + expect(c3.user).to eq user2 + end + end +end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index ecc19f5493..dddaf554a2 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -511,4 +511,43 @@ describe Spree::Order do end.to enqueue_job ConfirmOrderJob end end + + describe "associating a customer" do + let(:user) { create(:user) } + let(:distributor) { create(:distributor_enterprise) } + + context "when a user has been set on the order" do + let!(:order) { create(:order, distributor: distributor, user: user) } + context "and a customer for order.distributor and order.user.email already exists" do + let!(:customer) { create(:customer, enterprise: distributor, email: user.email) } + it "associates the order with the existing customer" do + order.send(:associate_customer) + expect(order.customer).to eq customer + end + end + context "and a customer for order.distributor and order.user.email does not alread exist" do + let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } + it "creates a new customer" do + expect{order.send(:associate_customer)}.to change{Customer.count}.by 1 + end + end + end + + context "when a user has not been set on the order" do + let!(:order) { create(:order, distributor: distributor, user: nil) } + context "and a customer for order.distributor and order.email already exists" do + let!(:customer) { create(:customer, enterprise: distributor, email: order.email) } + it "creates a new customer" do + order.send(:associate_customer) + expect(order.customer).to eq customer + end + end + context "and a customer for order.distributor and order.email does not alread exist" do + let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } + it "creates a new customer" do + expect{order.send(:associate_customer)}.to change{Customer.count}.by 1 + end + end + end + end end From ffac0e4cebc82670aae8305947ef3c299a09a9a9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 13 May 2015 16:06:38 +1000 Subject: [PATCH 39/56] Adding ngTagsInput to angular spec manifest --- spec/javascripts/application_spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index 10db2226a4..07202027b4 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -7,6 +7,7 @@ //= require angularjs-file-upload //= require lodash.underscore.js //= require angular-flash.min.js +//= require shared/ng-tags-input.min.js //= require shared/mm-foundation-tpls-0.2.2.min.js //= require textAngular.min.js //= require textAngular-sanitize.min.js From 3849b39d3e1b84d302d2f3d41d444c8eeb3cfddc Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 13 May 2015 16:39:43 +1000 Subject: [PATCH 40/56] Splitting out specs for pending changes service --- .../services/pending_changes_spec.js.coffee | 119 +++++++++++++++++ .../unit/bulk_order_management_spec.js.coffee | 123 +----------------- 2 files changed, 121 insertions(+), 121 deletions(-) create mode 100644 spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee diff --git a/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee new file mode 100644 index 0000000000..2acf684754 --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee @@ -0,0 +1,119 @@ +describe "Pending Changes", -> + dataSubmitter = pendingChanges = null + + beforeEach -> + dataSubmitter = jasmine.createSpy('dataSubmitter').andReturn { + then: (thenFn) -> + thenFn({propertyName: "new_value"}) + } + module 'admin.indexUtils', ($provide) -> + $provide.value 'dataSubmitter', dataSubmitter + return + + inject (_pendingChanges_) -> + pendingChanges = _pendingChanges_ + + + describe "adding a new change", -> + it "adds a new object with key of id if it does not already exist", -> + expect(pendingChanges.pendingChanges).toEqual {} + expect(pendingChanges.pendingChanges["1"]).not.toBeDefined() + pendingChanges.add 1, "propertyName", { a: 1 } + expect(pendingChanges.pendingChanges["1"]).toBeDefined() + + it "adds a new object with key of the altered attribute name if it does not already exist", -> + pendingChanges.add 1, "propertyName", { a: 1 } + expect(pendingChanges.pendingChanges["1"]).toBeDefined() + expect(pendingChanges.pendingChanges["1"]["propertyName"]).toEqual { a: 1 } + + it "replaces the existing object when adding a change to an attribute which already exists", -> + pendingChanges.add 1, "propertyName", { a: 1 } + expect(pendingChanges.pendingChanges["1"]).toBeDefined() + expect(pendingChanges.pendingChanges["1"]["propertyName"]).toEqual { a: 1 } + pendingChanges.add 1, "propertyName", { b: 2 } + expect(pendingChanges.pendingChanges["1"]["propertyName"]).toEqual { b: 2 } + + it "adds an attribute to key to a line item object when one already exists", -> + pendingChanges.add 1, "propertyName1", { a: 1 } + pendingChanges.add 1, "propertyName2", { b: 2 } + expect(pendingChanges.pendingChanges["1"]).toEqual { propertyName1: { a: 1}, propertyName2: { b: 2 } } + + describe "removing all existing changes", -> + it "resets pendingChanges object", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + expect(pendingChanges.pendingChanges["1"]["propertyName2"]).toBeDefined() + pendingChanges.removeAll() + expect(pendingChanges.pendingChanges["1"]).not.toBeDefined() + expect(pendingChanges.pendingChanges).toEqual {} + + describe "removing an existing change", -> + it "deletes a change if it exists", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + pendingChanges.remove 1, "propertyName1" + expect(pendingChanges.pendingChanges["1"]).toBeDefined() + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).not.toBeDefined() + + it "deletes a line item object if it is empty", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + pendingChanges.remove 1, "propertyName1" + expect(pendingChanges.pendingChanges["1"]).not.toBeDefined() + + it "does nothing if key with specified attribute does not exist", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + pendingChanges.remove 1, "propertyName2" + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toEqual { a: 1 } + + it "does nothing if key with specified id does not exist", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + pendingChanges.remove 2, "propertyName1" + expect(pendingChanges.pendingChanges["1"]).toEqual { "propertyName1": { a: 1 } } + + describe "submitting an individual change to the server", -> + change = null + beforeEach -> + object = {id: 1} + scope = { reset: jasmine.createSpy('reset') }; + attr = "propertyName" + change = { object: object, scope: scope, attr: attr } + + it "sends the correct object to dataSubmitter", -> + pendingChanges.submit change + expect(dataSubmitter.calls.length).toEqual 1 + expect(dataSubmitter).toHaveBeenCalledWith change + + it "calls remove with id and attribute name", -> + spyOn(pendingChanges, "remove").andCallFake(->) + pendingChanges.submit change + expect(pendingChanges.remove.calls.length).toEqual 1 + expect(pendingChanges.remove).toHaveBeenCalledWith 1, "propertyName" + + it "calls reset on the relevant scope", -> + pendingChanges.submit change + expect(change.scope.reset).toHaveBeenCalledWith "new_value" + + describe "cycling through all changes to submit to server", -> + it "sends the correct object to dataSubmitter", -> + spyOn(pendingChanges, "submit").andCallFake(->) + pendingChanges.pendingChanges = + 1: { "prop1": { attr: "prop1", value: 1 }, "prop2": { attr: "prop2", value: 2 } } + 2: { "prop1": { attr: "prop1", value: 2 }, "prop2": { attr: "prop2", value: 4 } } + 7: { "prop2": { attr: "prop2", value: 5 } } + pendingChanges.submitAll() + expect(pendingChanges.submit.calls.length).toEqual 5 + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop1", value: 1 } + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 2 } + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop1", value: 2 } + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 4 } + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 5 } + + it "returns an array of promises representing all sumbit requests", -> + spyOn(pendingChanges, "submit").andCallFake (change) -> change.value + pendingChanges.pendingChanges = + 1: { "prop1": { attr: "prop1", value: 1 } } + 2: { "prop1": { attr: "prop1", value: 2 }, "prop2": { attr: "prop1", value: 4 } } + expect(pendingChanges.submitAll()).toEqual [ 1, 2, 4 ] diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index 35b9e13d61..cbfc929e01 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -33,8 +33,8 @@ describe "AdminOrderMgmtCtrl", -> httpBackend.flush() expect(scope.suppliers).toEqual [{ id : '0', name : 'All' }, 'list of suppliers'] - expect(scope.distributors).toEqual [ { id : '0', name : 'All' }, 'list of distributors' ] - expect(scope.orderCycles).toEqual [ { id : '0', name : 'All' }, 'oc1', 'oc2', 'oc3' ] + expect(scope.distributors).toEqual [ { id : '0', name : 'All' }, 'list of distributors' ] + expect(scope.orderCycles).toEqual [ { id : '0', name : 'All' }, 'oc1', 'oc2', 'oc3' ] expect(scope.initialiseVariables.calls.length).toBe 1 expect(scope.fetchOrders.calls.length).toBe 1 @@ -350,125 +350,6 @@ describe "AdminOrderMgmtCtrl", -> spyOn(VariantUnitManager, "getUnitName").andReturn "kg" expect(scope.formattedValueWithUnitName(2000,unitsVariant)).toEqual "2 kg" -describe "managing pending changes", -> - dataSubmitter = pendingChangesService = null - - beforeEach -> - dataSubmitter = jasmine.createSpy('dataSubmitter').andReturn { - then: (thenFn) -> - thenFn({propertyName: "new_value"}) - } - - beforeEach -> - module "ofn.admin", ($provide) -> - $provide.value 'dataSubmitter', dataSubmitter - return - - beforeEach inject (pendingChanges) -> - pendingChangesService = pendingChanges - - describe "adding a new change", -> - it "adds a new object with key of id if it does not already exist", -> - expect(pendingChangesService.pendingChanges).toEqual {} - expect(pendingChangesService.pendingChanges["1"]).not.toBeDefined() - pendingChangesService.add 1, "propertyName", { a: 1 } - expect(pendingChangesService.pendingChanges["1"]).toBeDefined() - - it "adds a new object with key of the altered attribute name if it does not already exist", -> - pendingChangesService.add 1, "propertyName", { a: 1 } - expect(pendingChangesService.pendingChanges["1"]).toBeDefined() - expect(pendingChangesService.pendingChanges["1"]["propertyName"]).toEqual { a: 1 } - - it "replaces the existing object when adding a change to an attribute which already exists", -> - pendingChangesService.add 1, "propertyName", { a: 1 } - expect(pendingChangesService.pendingChanges["1"]).toBeDefined() - expect(pendingChangesService.pendingChanges["1"]["propertyName"]).toEqual { a: 1 } - pendingChangesService.add 1, "propertyName", { b: 2 } - expect(pendingChangesService.pendingChanges["1"]["propertyName"]).toEqual { b: 2 } - - it "adds an attribute to key to a line item object when one already exists", -> - pendingChangesService.add 1, "propertyName1", { a: 1 } - pendingChangesService.add 1, "propertyName2", { b: 2 } - expect(pendingChangesService.pendingChanges["1"]).toEqual { propertyName1: { a: 1}, propertyName2: { b: 2 } } - - describe "removing all existing changes", -> - it "resets pendingChanges object", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - expect(pendingChangesService.pendingChanges["1"]["propertyName2"]).toBeDefined() - pendingChangesService.removeAll() - expect(pendingChangesService.pendingChanges["1"]).not.toBeDefined() - expect(pendingChangesService.pendingChanges).toEqual {} - - describe "removing an existing change", -> - it "deletes a change if it exists", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - pendingChangesService.remove 1, "propertyName1" - expect(pendingChangesService.pendingChanges["1"]).toBeDefined() - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).not.toBeDefined() - - it "deletes a line item object if it is empty", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - pendingChangesService.remove 1, "propertyName1" - expect(pendingChangesService.pendingChanges["1"]).not.toBeDefined() - - it "does nothing if key with specified attribute does not exist", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - pendingChangesService.remove 1, "propertyName2" - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toEqual { a: 1 } - - it "does nothing if key with specified id does not exist", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - pendingChangesService.remove 2, "propertyName1" - expect(pendingChangesService.pendingChanges["1"]).toEqual { "propertyName1": { a: 1 } } - - describe "submitting an individual change to the server", -> - it "sends the correct object to dataSubmitter", -> - changeObj = { element: {} } - pendingChangesService.submit 1, "propertyName", changeObj - expect(dataSubmitter.calls.length).toEqual 1 - expect(dataSubmitter).toHaveBeenCalledWith changeObj - - it "calls remove with id and attribute name", -> - changeObj = { element: {} } - spyOn(pendingChangesService, "remove").andCallFake(->) - pendingChangesService.submit 1, "propertyName", changeObj - expect(pendingChangesService.remove.calls.length).toEqual 1 - expect(pendingChangesService.remove).toHaveBeenCalledWith 1, "propertyName" - - it "resets the dbValue attribute of the element in question", -> - element = { dbValue: 2 } - changeObj = { element: element } - pendingChangesService.submit 1, "propertyName", changeObj - expect(element.dbValue).toEqual "new_value" - - describe "cycling through all changes to submit to server", -> - it "sends the correct object to dataSubmitter", -> - spyOn(pendingChangesService, "submit").andCallFake(->) - pendingChangesService.pendingChanges = - 1: { "prop1": 1, "prop2": 2 } - 2: { "prop1": 2, "prop2": 4 } - 7: { "prop2": 5 } - pendingChangesService.submitAll() - expect(pendingChangesService.submit.calls.length).toEqual 5 - expect(pendingChangesService.submit).toHaveBeenCalledWith '1', "prop1", 1 - expect(pendingChangesService.submit).toHaveBeenCalledWith '1', "prop2", 2 - expect(pendingChangesService.submit).toHaveBeenCalledWith '2', "prop1", 2 - expect(pendingChangesService.submit).toHaveBeenCalledWith '2', "prop2", 4 - expect(pendingChangesService.submit).toHaveBeenCalledWith '7', "prop2", 5 - - it "returns an array of promises representing all sumbit requests", -> - spyOn(pendingChangesService, "submit").andCallFake (id,attrName,changeObj) -> - id - pendingChangesService.pendingChanges = - 1: { "prop1": 1 } - 2: { "prop1": 2, "prop2": 4 } - expect(pendingChangesService.submitAll()).toEqual [ '1','2','2' ] - describe "dataSubmitter service", -> qMock = httpMock = {} switchClassSpy = resolveSpy = rejectSpy = dataSubmitterService = null From 640c02570d49a08a5ef233e2cb3f8dae76880dac Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 13 May 2015 16:41:28 +1000 Subject: [PATCH 41/56] Splitting out specs for switchClass service --- .../services/switch_class_spec.js.coffee | 52 ++++++++++++++++++ .../unit/bulk_order_management_spec.js.coffee | 53 ------------------- 2 files changed, 52 insertions(+), 53 deletions(-) create mode 100644 spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee diff --git a/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee new file mode 100644 index 0000000000..e7dedb4e92 --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee @@ -0,0 +1,52 @@ +describe "switchClass service", -> + elementMock = timeoutMock = {} + removeClass = addClass = switchClassService = null + + beforeEach -> + addClass = jasmine.createSpy('addClass') + removeClass = jasmine.createSpy('removeClass') + elementMock = + addClass: addClass + removeClass: removeClass + timeoutMock = jasmine.createSpy('timeout').andReturn "new timeout" + timeoutMock.cancel = jasmine.createSpy('timeout.cancel') + + beforeEach -> + module "ofn.admin" , ($provide) -> + $provide.value '$timeout', timeoutMock + return + + beforeEach inject (switchClass) -> + switchClassService = switchClass + + it "calls addClass on the element once", -> + switchClassService elementMock, "addClass", [], false + expect(addClass).toHaveBeenCalledWith "addClass" + expect(addClass.calls.length).toEqual 1 + + it "calls removeClass on the element for ", -> + switchClassService elementMock, "", ["remClass1", "remClass2", "remClass3"], false + expect(removeClass).toHaveBeenCalledWith "remClass1" + expect(removeClass).toHaveBeenCalledWith "remClass2" + expect(removeClass).toHaveBeenCalledWith "remClass3" + expect(removeClass.calls.length).toEqual 3 + + it "call cancel on element.timout only if it exists", -> + switchClassService elementMock, "", [], false + expect(timeoutMock.cancel).not.toHaveBeenCalled() + elementMock.timeout = true + switchClassService elementMock, "", [], false + expect(timeoutMock.cancel).toHaveBeenCalled() + + it "doesn't set up a new timeout if 'timeout' is false", -> + switchClassService elementMock, "class1", ["class2"], false + expect(timeoutMock).not.toHaveBeenCalled() + + it "doesn't set up a new timeout if 'timeout' is a string", -> + switchClassService elementMock, "class1", ["class2"], "string" + expect(timeoutMock).not.toHaveBeenCalled() + + it "sets up a new timeout if 'timeout' parameter is an integer", -> + switchClassService elementMock, "class1", ["class2"], 1000 + expect(timeoutMock).toHaveBeenCalled() + expect(elementMock.timeout).toEqual "new timeout" diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index cbfc929e01..87f9903e05 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -407,59 +407,6 @@ describe "dataSubmitter service", -> expect(rejectSpy.calls.length).toEqual 1 expect(switchClassSpy).toHaveBeenCalledWith element, "update-error", ["update-pending", "update-success"], false -describe "switchClass service", -> - elementMock = timeoutMock = {} - removeClass = addClass = switchClassService = null - - beforeEach -> - addClass = jasmine.createSpy('addClass') - removeClass = jasmine.createSpy('removeClass') - elementMock = - addClass: addClass - removeClass: removeClass - timeoutMock = jasmine.createSpy('timeout').andReturn "new timeout" - timeoutMock.cancel = jasmine.createSpy('timeout.cancel') - - beforeEach -> - module "ofn.admin" , ($provide) -> - $provide.value '$timeout', timeoutMock - return - - beforeEach inject (switchClass) -> - switchClassService = switchClass - - it "calls addClass on the element once", -> - switchClassService elementMock, "addClass", [], false - expect(addClass).toHaveBeenCalledWith "addClass" - expect(addClass.calls.length).toEqual 1 - - it "calls removeClass on the element for ", -> - switchClassService elementMock, "", ["remClass1", "remClass2", "remClass3"], false - expect(removeClass).toHaveBeenCalledWith "remClass1" - expect(removeClass).toHaveBeenCalledWith "remClass2" - expect(removeClass).toHaveBeenCalledWith "remClass3" - expect(removeClass.calls.length).toEqual 3 - - it "call cancel on element.timout only if it exists", -> - switchClassService elementMock, "", [], false - expect(timeoutMock.cancel).not.toHaveBeenCalled() - elementMock.timeout = true - switchClassService elementMock, "", [], false - expect(timeoutMock.cancel).toHaveBeenCalled() - - it "doesn't set up a new timeout if 'timeout' is false", -> - switchClassService elementMock, "class1", ["class2"], false - expect(timeoutMock).not.toHaveBeenCalled() - - it "doesn't set up a new timeout if 'timeout' is a string", -> - switchClassService elementMock, "class1", ["class2"], "string" - expect(timeoutMock).not.toHaveBeenCalled() - - it "sets up a new timeout if 'timeout' parameter is an integer", -> - switchClassService elementMock, "class1", ["class2"], 1000 - expect(timeoutMock).toHaveBeenCalled() - expect(elementMock.timeout).toEqual "new timeout" - describe "Auxiliary functions", -> describe "getting a zero filled two digit number", -> it "returns the number as a string if its value is greater than or equal to 10", -> From 50d0d04994962ab5f8900e699fa58847402f47f6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 14 May 2015 11:38:55 +1000 Subject: [PATCH 42/56] Removing obsolete dataSubmitter service --- .../services/data_submitter.js.coffee | 10 --- .../services/pending_changes.js.coffee | 7 +- .../services/pending_changes_spec.js.coffee | 64 ++++++++++++++----- .../unit/bulk_order_management_spec.js.coffee | 57 ----------------- 4 files changed, 52 insertions(+), 86 deletions(-) delete mode 100644 app/assets/javascripts/admin/index_utils/services/data_submitter.js.coffee diff --git a/app/assets/javascripts/admin/index_utils/services/data_submitter.js.coffee b/app/assets/javascripts/admin/index_utils/services/data_submitter.js.coffee deleted file mode 100644 index 934ae4a07e..0000000000 --- a/app/assets/javascripts/admin/index_utils/services/data_submitter.js.coffee +++ /dev/null @@ -1,10 +0,0 @@ -angular.module("admin.indexUtils").factory "dataSubmitter", ($http, $q, resources) -> - return (change) -> - deferred = $q.defer() - resources.update(change).$promise.then (data) -> - change.scope.success() - deferred.resolve data - , -> - change.scope.error() - deferred.reject() - deferred.promise diff --git a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee index 86a6b2821f..2f40a7faef 100644 --- a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.indexUtils").factory "pendingChanges", (dataSubmitter) -> +angular.module("admin.indexUtils").factory "pendingChanges", (resources) -> new class pendingChanges pendingChanges: {} @@ -22,9 +22,12 @@ angular.module("admin.indexUtils").factory "pendingChanges", (dataSubmitter) -> all submit: (change) -> - dataSubmitter(change).then (data) => + resources.update(change).$promise.then (data) => @remove change.object.id, change.attr change.scope.reset( data["#{change.attr}"] ) + change.scope.success() + , (error) -> + change.scope.error() changeCount: (objectChanges) -> Object.keys(objectChanges).length diff --git a/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee index 2acf684754..31b85df217 100644 --- a/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee @@ -1,13 +1,17 @@ describe "Pending Changes", -> - dataSubmitter = pendingChanges = null + resourcesMock = pendingChanges = null beforeEach -> - dataSubmitter = jasmine.createSpy('dataSubmitter').andReturn { - then: (thenFn) -> - thenFn({propertyName: "new_value"}) - } + + resourcesMock = + update: jasmine.createSpy('update').andCallFake (change) -> + $promise: + then: (successFn, errorFn) -> + return successFn({propertyName: "new_value"}) if change.success + errorFn("error") + module 'admin.indexUtils', ($provide) -> - $provide.value 'dataSubmitter', dataSubmitter + $provide.value 'resources', resourcesMock return inject (_pendingChanges_) -> @@ -77,24 +81,50 @@ describe "Pending Changes", -> change = null beforeEach -> object = {id: 1} - scope = { reset: jasmine.createSpy('reset') }; + scope = { reset: jasmine.createSpy('reset'), success: jasmine.createSpy('success'), error: jasmine.createSpy('error') }; attr = "propertyName" change = { object: object, scope: scope, attr: attr } + it "sends the correct object to dataSubmitter", -> pendingChanges.submit change - expect(dataSubmitter.calls.length).toEqual 1 - expect(dataSubmitter).toHaveBeenCalledWith change + expect(resourcesMock.update.calls.length).toEqual 1 + expect(resourcesMock.update).toHaveBeenCalledWith change - it "calls remove with id and attribute name", -> - spyOn(pendingChanges, "remove").andCallFake(->) - pendingChanges.submit change - expect(pendingChanges.remove.calls.length).toEqual 1 - expect(pendingChanges.remove).toHaveBeenCalledWith 1, "propertyName" + describe "successful request", -> + beforeEach -> + change.success = true - it "calls reset on the relevant scope", -> - pendingChanges.submit change - expect(change.scope.reset).toHaveBeenCalledWith "new_value" + it "calls remove with id and attribute name", -> + spyOn(pendingChanges, "remove").andCallFake(->) + pendingChanges.submit change + expect(pendingChanges.remove.calls.length).toEqual 1 + expect(pendingChanges.remove).toHaveBeenCalledWith 1, "propertyName" + + it "calls reset on the relevant scope", -> + pendingChanges.submit change + expect(change.scope.reset).toHaveBeenCalledWith "new_value" + + it "calls success on the relevant scope", -> + pendingChanges.submit change + expect(change.scope.success).toHaveBeenCalled() + + describe "unsuccessful request", -> + beforeEach -> + change.success = false + + it "does not call remove", -> + spyOn(pendingChanges, "remove").andCallFake(->) + pendingChanges.submit change + expect(pendingChanges.remove).not.toHaveBeenCalled() + + it "does not call reset on the relevant scope", -> + pendingChanges.submit change + expect(change.scope.reset).not.toHaveBeenCalled() + + it "calls error on the relevant scope", -> + pendingChanges.submit change + expect(change.scope.error).toHaveBeenCalled() describe "cycling through all changes to submit to server", -> it "sends the correct object to dataSubmitter", -> diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index 87f9903e05..abf0b93bd7 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -350,63 +350,6 @@ describe "AdminOrderMgmtCtrl", -> spyOn(VariantUnitManager, "getUnitName").andReturn "kg" expect(scope.formattedValueWithUnitName(2000,unitsVariant)).toEqual "2 kg" -describe "dataSubmitter service", -> - qMock = httpMock = {} - switchClassSpy = resolveSpy = rejectSpy = dataSubmitterService = null - - beforeEach -> - resolveSpy = jasmine.createSpy('resolve') - rejectSpy = jasmine.createSpy('reject') - qMock.defer = -> - resolve: resolveSpy - reject: rejectSpy - promise: "promise1" - - # Can't use httpBackend because the qMock interferes with it - httpMock.put = (url) -> - success: (successFn) -> - successFn("somedata") if url == "successURL" - error: (errorFn) -> - errorFn() if url == "errorURL" - - spyOn(httpMock, "put").andCallThrough() - spyOn(qMock, "defer").andCallThrough() - - switchClassSpy = jasmine.createSpy('switchClass') - - beforeEach -> - module "ofn.admin" , ($provide) -> - $provide.value '$q', qMock - $provide.value '$http', httpMock - $provide.value 'switchClass', switchClassSpy - return - - beforeEach inject (dataSubmitter) -> - dataSubmitterService = dataSubmitter - - it "returns a promise", -> - expect(dataSubmitterService( { url: "successURL" } )).toEqual "promise1" - expect(qMock.defer).toHaveBeenCalled() - - it "sends a PUT request with the url property of changeObj", -> - dataSubmitterService { url: "successURL" } - expect(httpMock.put).toHaveBeenCalledWith "successURL" - - it "calls resolve on deferred object when request is successful", -> - element = { a: 1 } - dataSubmitterService { url: "successURL", element: element } - expect(resolveSpy.calls.length).toEqual 1 - expect(rejectSpy.calls.length).toEqual 0 - expect(resolveSpy).toHaveBeenCalledWith "somedata" - expect(switchClassSpy).toHaveBeenCalledWith element, "update-success", ["update-pending", "update-error"], 3000 - - it "calls reject on deferred object when request is erroneous", -> - element = { b: 2 } - dataSubmitterService { url: "errorURL", element: element } - expect(resolveSpy.calls.length).toEqual 0 - expect(rejectSpy.calls.length).toEqual 1 - expect(switchClassSpy).toHaveBeenCalledWith element, "update-error", ["update-pending", "update-success"], false - describe "Auxiliary functions", -> describe "getting a zero filled two digit number", -> it "returns the number as a string if its value is greater than or equal to 10", -> From 78fc3e376bb36482c88ee1cd91c07fced7cdcf20 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 14 May 2015 11:39:37 +1000 Subject: [PATCH 43/56] Fixing styling of inputs for with pending/success/error status --- app/assets/stylesheets/admin/orders.css.scss | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 249447757c..b1327630c6 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -8,16 +8,22 @@ } } -.update-pending { - border: solid 1px orange; +input, div { + &.update-pending { + border: solid 1px orange; + } } -.update-error { - border: solid 1px red; +input, div { + &.update-error { + border: solid 1px red; + } } -.update-success { - border: solid 1px #9fc820; +input, div { + &.update-success { + border: solid 1px #9fc820; + } } .no-close .ui-dialog-titlebar-close { From c8502747beebbdfb57284139e465a6932c155b01 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 14 May 2015 12:37:55 +1000 Subject: [PATCH 44/56] Fixing layout of customers index --- .../controllers/customers_controller.js.coffee | 3 ++- app/views/admin/customers/index.html.haml | 15 +++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index 0e82806ef9..be526a4349 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -8,7 +8,8 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, Customers code: { name: "Code", visible: true } tags: { name: "Tags", visible: true } - $scope.initialise = -> + $scope.$watch "shop", -> + Customers.loaded = false $scope.customers = Customers.index(enterprise_id: $scope.shop.id) $scope.loaded = -> diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index f3e82f9cdf..66790a34df 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -4,15 +4,14 @@ = admin_inject_shops %div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } } - .row{ ng: { hide: "loaded()" } } - .two.columns.alpha - Hub + .row{ ng: { hide: "loaded() && filteredCustomers.length > 0" } } + .five.columns.alpha + %h3 Please select a Hub: .four.columns %select.select2.fullwidth#shop_id{ 'ng-model' => 'shop.id', name: 'shop_id', 'ng-options' => 'shop.id as shop.name for shop in shops' } - .ten.columns.omega - %input{ type: 'button', value: 'Go', ng: { click: 'initialise()' } } + .seven.columns.omega   - .row{ 'ng-hide' => '!loaded() || lineItems.length == 0' } + .row{ 'ng-hide' => '!loaded() || filteredCustomers.length == 0' } .controls{ :class => "sixteen columns alpha", :style => "margin-bottom: 15px;" } .five.columns.alpha %input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } @@ -32,7 +31,7 @@ %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true } %span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "✓" || !column.visible && " " }} %span{ :class => 'two columns omega' } {{column.name }} - .row{ 'ng-if' => 'shop_id && !loaded()' } + .row{ 'ng-if' => 'shop && !loaded()' } .sixteen.columns.alpha#loading %img.spinner{ src: "/assets/spinning-circles.svg" } %h1 LOADING CUSTOMERS @@ -40,7 +39,7 @@ %h1#no_results No customers found. - .row{ ng: { show: "loaded()" } } + .row{ ng: { show: "loaded() && filteredCustomers.length > 0" } } %form{ name: "customers" } %table.index#customers %col.email{ width: "20%"} From 50ae331d9459af558319558ea913b5a5562aaa5c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 29 May 2015 16:03:16 +1000 Subject: [PATCH 45/56] ng-cloak mobile menu --- app/views/shared/menu/_mobile_menu.html.haml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/shared/menu/_mobile_menu.html.haml b/app/views/shared/menu/_mobile_menu.html.haml index 5ed57917fd..e5ba9af83b 100644 --- a/app/views/shared/menu/_mobile_menu.html.haml +++ b/app/views/shared/menu/_mobile_menu.html.haml @@ -2,7 +2,7 @@ %section.left %a.left-off-canvas-toggle.menu-icon %span - %section.right + %section.right{"ng-cloak" => true} .cart = render partial: "shared/menu/cart" %a{href: main_app.shop_path} @@ -11,34 +11,34 @@ %aside.left-off-canvas-menu.show-for-medium-down %ul.off-canvas-list %li.ofn-logo - %a{href: root_path} + %a{href: root_path} %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"} - + - if current_page? root_path %li.li-menu %a{"ofn-scroll-to" => "hubs"} - %span.nav-primary + %span.nav-primary %i.ofn-i_040-hub Hubs - else %li.li-menu %a{href: root_path + "#/#hubs"} - %span.nav-primary + %span.nav-primary %i.ofn-i_040-hub Hubs %li.li-menu %a{href: main_app.map_path} - %span.nav-primary + %span.nav-primary %i.ofn-i_037-map Map %li.li-menu %a{href: main_app.producers_path} - %span.nav-primary + %span.nav-primary %i.ofn-i_036-producers Producers %li.li-menu %a{href: main_app.groups_path} - %span.nav-primary + %span.nav-primary %i.ofn-i_035-groups Groups From 5c3a59acabaf2a340e1fc084e221eeac5cba84ad Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 29 May 2015 16:39:41 +1000 Subject: [PATCH 46/56] ng-cloak order cycles selector, tabs and shopfront --- app/views/enterprises/shop.html.haml | 2 +- app/views/shop/products/_form.html.haml | 2 +- app/views/shopping_shared/_tabs.html.haml | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/enterprises/shop.html.haml b/app/views/enterprises/shop.html.haml index 89ce1a30ac..08e25417cb 100644 --- a/app/views/enterprises/shop.html.haml +++ b/app/views/enterprises/shop.html.haml @@ -3,7 +3,7 @@ %shop.darkswarm - content_for :order_cycle_form do - %div{"ng-controller" => "OrderCycleChangeCtrl"} + %div{"ng-controller" => "OrderCycleChangeCtrl", "ng-cloak" => true} %closing{"ng-if" => "OrderCycle.selected()"} Next order closing %strong {{ OrderCycle.orders_close_at() | date_in_words }} diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 0df4f022aa..bdaded0f0d 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -1,4 +1,4 @@ -%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", +%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true, "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} // TODO: Needs an ng-show to slide content down diff --git a/app/views/shopping_shared/_tabs.html.haml b/app/views/shopping_shared/_tabs.html.haml index 3641e0cbd5..48c0a1d7b5 100644 --- a/app/views/shopping_shared/_tabs.html.haml +++ b/app/views/shopping_shared/_tabs.html.haml @@ -1,11 +1,11 @@ -#tabs{"ng-controller" => "TabsCtrl"} +#tabs{"ng-controller" => "TabsCtrl", "ng-cloak" => true} .row %tabset -# Build all tabs. - - for name, heading_cols in { about: ["About #{current_distributor.name}", 6], - producers: ["Producers",2], + - for name, heading_cols in { about: ["About #{current_distributor.name}", 6], + producers: ["Producers",2], contact: ["Contact",2], - groups: ["Groups",2]} + groups: ["Groups",2]} -# tabs take tab path in 'active' and 'select' functions defined in TabsCtrl. - heading, cols = heading_cols %tab.columns{heading: heading, From 0d3cdb9c694b0346912acf0064b008b5b2cfa17a Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 29 May 2015 16:51:55 +1000 Subject: [PATCH 47/56] Expand All button to show all variants in BPE --- .../javascripts/admin/bulk_product_update.js.coffee | 6 ++++++ .../admin/directives/toggle_variants.js.coffee | 4 +--- .../admin/services/display_properties.js.coffee | 8 +++----- .../admin/products/bulk_edit/_products_head.html.haml | 2 ++ .../admin/products/bulk_edit/_products_product.html.haml | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index c902328af4..c557c0c791 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -109,6 +109,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit" + $scope.toggleShowAllVariants = -> + showVariants = !DisplayProperties.showVariants 0 + $scope.filteredProducts.forEach (product) -> + DisplayProperties.setShowVariants product.id, showVariants + DisplayProperties.setShowVariants 0, showVariants + $scope.addVariant = (product) -> product.variants.push id: $scope.nextVariantId() diff --git a/app/assets/javascripts/admin/directives/toggle_variants.js.coffee b/app/assets/javascripts/admin/directives/toggle_variants.js.coffee index 410df8d7e9..f5b18ae5cb 100644 --- a/app/assets/javascripts/admin/directives/toggle_variants.js.coffee +++ b/app/assets/javascripts/admin/directives/toggle_variants.js.coffee @@ -1,10 +1,8 @@ angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) -> link: (scope, element, attrs) -> if DisplayProperties.showVariants scope.product.id - element.removeClass "icon-chevron-right" element.addClass "icon-chevron-down" else - element.removeClass "icon-chevron-down" element.addClass "icon-chevron-right" element.on "click", -> @@ -16,4 +14,4 @@ angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) - else DisplayProperties.setShowVariants scope.product.id, true element.removeClass "icon-chevron-right" - element.addClass "icon-chevron-down" \ No newline at end of file + element.addClass "icon-chevron-down" diff --git a/app/assets/javascripts/admin/services/display_properties.js.coffee b/app/assets/javascripts/admin/services/display_properties.js.coffee index 7288706032..3037c9f068 100644 --- a/app/assets/javascripts/admin/services/display_properties.js.coffee +++ b/app/assets/javascripts/admin/services/display_properties.js.coffee @@ -3,12 +3,10 @@ angular.module("ofn.admin").factory "DisplayProperties", -> displayProperties: {} showVariants: (product_id) -> - @initProduct product_id - @displayProperties[product_id].showVariants + @productProperties(product_id).showVariants setShowVariants: (product_id, showVariants) -> - @initProduct product_id - @displayProperties[product_id].showVariants = showVariants + @productProperties(product_id).showVariants = showVariants - initProduct: (product_id) -> + productProperties: (product_id) -> @displayProperties[product_id] ||= {showVariants: false} diff --git a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml index 6e22aef8ff..97b4f47c83 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml @@ -19,6 +19,8 @@ %thead %tr %th.left-actions + %a{ 'ng-click' => 'toggleShowAllVariants()', :style => 'color: red' } + Expand All %th.producer{ 'ng-show' => 'columns.producer.visible' } Producer %th.sku{ 'ng-show' => 'columns.sku.visible' } SKU %th.name{ 'ng-show' => 'columns.name.visible' } Name diff --git a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml index 376e88071a..6ac25ae286 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml @@ -1,6 +1,6 @@ %tr.product{ :id => "p_{{product.id}}" } %td.left-actions - %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' } + %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants", 'ng-show' => 'hasVariants(product)' } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" } %td.producer{ 'ng-show' => 'columns.producer.visible' } %select.select2.fullwidth{ 'ng-model' => 'product.producer_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' } From db47c01784df955cff00560f3c4b5454467025dd Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 1 May 2015 09:50:12 +1000 Subject: [PATCH 48/56] Initial config for parallel spec running --- .rspec_parallel | 2 ++ Gemfile | 1 + Gemfile.lock | 4 ++++ config/database.yml | 2 +- 4 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .rspec_parallel diff --git a/.rspec_parallel b/.rspec_parallel new file mode 100644 index 0000000000..54b33804ae --- /dev/null +++ b/.rspec_parallel @@ -0,0 +1,2 @@ +--format progress +--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log diff --git a/Gemfile b/Gemfile index b63961a3ef..3a698cf366 100644 --- a/Gemfile +++ b/Gemfile @@ -112,4 +112,5 @@ group :development do gem 'guard-rails' gem 'guard-zeus' gem 'guard-rspec' + gem 'parallel_tests' end diff --git a/Gemfile.lock b/Gemfile.lock index 1b62201120..09c5f866f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -362,6 +362,9 @@ GEM activesupport (>= 3.0.0) cocaine (~> 0.5.3) mime-types + parallel (1.4.1) + parallel_tests (1.3.7) + parallel paypal-sdk-core (0.2.10) multi_json (~> 1.0) xml-simple @@ -575,6 +578,7 @@ DEPENDENCIES newrelic_rpm oj paperclip + parallel_tests pg poltergeist pry-debugger diff --git a/config/database.yml b/config/database.yml index d74ed6256a..7eef2396f9 100644 --- a/config/database.yml +++ b/config/database.yml @@ -10,7 +10,7 @@ development: test: adapter: postgresql encoding: unicode - database: open_food_network_test + database: open_food_network_test<%= ENV['TEST_ENV_NUMBER'] %> pool: 5 host: localhost username: ofn From 2b3689fd933f99349f5284c61fc51ff1e875fb34 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 14:29:24 +1000 Subject: [PATCH 49/56] Run CI specs in parallel --- script/ci/run_tests.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/script/ci/run_tests.sh b/script/ci/run_tests.sh index 5539935a8b..9f9cdfa30d 100755 --- a/script/ci/run_tests.sh +++ b/script/ci/run_tests.sh @@ -13,7 +13,8 @@ echo "--- Bundling" bundle install echo "--- Loading test database" -bundle exec rake db:test:load +bundle exec rake db:drop db:create db:schema:load +bundle exec rake parallel:drop parallel:create parallel:load_schema echo "--- Running tests" -bundle exec rspec spec +bundle exec rake parallel:spec From 73029636055c609959feeca03d29a621eb1a1124 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 3 Jun 2015 15:11:32 +1000 Subject: [PATCH 50/56] inventory report: filter was broken because filter_to_order_cycle returned nil [skip ci] --- .../products_and_inventory_report.rb | 14 ++------------ .../products_and_inventory_report_spec.rb | 12 +----------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb index 163f9a4f50..2b797bd944 100644 --- a/lib/open_food_network/products_and_inventory_report.rb +++ b/lib/open_food_network/products_and_inventory_report.rb @@ -47,7 +47,7 @@ module OpenFoodNetwork end def variants - filter(child_variants) + filter(master_variants) + filter(child_variants) end def child_variants @@ -57,16 +57,6 @@ module OpenFoodNetwork .order("spree_products.name") end - def master_variants - Spree::Variant.where(:is_master => true) - .joins(:product) - .where("(select spree_variants.id from spree_variants as other_spree_variants - WHERE other_spree_variants.product_id = spree_variants.product_id - AND other_spree_variants.is_master = 'f' LIMIT 1) IS NULL") - .merge(visible_products) - .order("spree_products.name") - end - def filter(variants) # NOTE: Ordering matters. # filter_to_order_cycle and filter_to_distributor return Arrays not Arel @@ -107,7 +97,7 @@ module OpenFoodNetwork def filter_to_order_cycle(variants) if params[:order_cycle_id].to_i > 0 order_cycle = OrderCycle.find params[:order_cycle_id] - variants.select! { |v| order_cycle.variants.include? v } + variants.select { |v| order_cycle.variants.include? v } else variants end diff --git a/spec/lib/open_food_network/products_and_inventory_report_spec.rb b/spec/lib/open_food_network/products_and_inventory_report_spec.rb index 368eb4d4d7..13796c10f6 100644 --- a/spec/lib/open_food_network/products_and_inventory_report_spec.rb +++ b/spec/lib/open_food_network/products_and_inventory_report_spec.rb @@ -54,10 +54,8 @@ module OpenFoodNetwork it "fetches variants for some params" do subject.should_receive(:child_variants).and_return ["children"] - subject.should_receive(:master_variants).and_return ["masters"] subject.should_receive(:filter).with(['children']).and_return ["filter_children"] - subject.should_receive(:filter).with(['masters']).and_return ["filter_masters"] - subject.variants.should == ["filter_children", "filter_masters"] + subject.variants.should == ["filter_children"] end end @@ -92,14 +90,6 @@ module OpenFoodNetwork end end - describe "fetching master variants" do - it "doesn't return master variants with siblings" do - product = create(:simple_product, supplier: supplier) - - subject.master_variants.should be_empty - end - end - describe "Filtering variants" do let(:variants) { Spree::Variant.scoped.joins(:product).where(is_master: false) } it "should return unfiltered variants sans-params" do From 09160c8ea67768afeca2c54a186586b29da4796e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 3 Jun 2015 15:23:55 +0800 Subject: [PATCH 51/56] Fixing customers controller spec --- .../controllers/customers_controller_spec.js.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index ced19c8b33..22777a6528 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -12,11 +12,11 @@ describe "CustomersCtrl", -> Customers = _Customers_ ctrl = $controller 'customersCtrl', {$scope: scope, Customers: Customers, shops: shops} - describe "initialise()", -> + describe "setting the shop on scope", -> beforeEach -> spyOn(Customers, "index").andReturn "list of customers" - scope.shop = {id: 1} - scope.initialise() + scope.$apply -> + scope.shop = {id: 1} it "calls Customers#index with the correct params", -> expect(Customers.index).toHaveBeenCalledWith({enterprise_id: 1}) From 7c9b4dbc3e395736f717622df80eb5442afe4e64 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 3 Jun 2015 15:33:04 +0800 Subject: [PATCH 52/56] Resolving unmerged section in bulk order management template --- .../admin/orders/bulk_management.html.haml | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index ded07435f6..71134e38a9 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -132,7 +132,6 @@ %th.actions Ask?  %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } -<<<<<<< HEAD %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } %td.bulk %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } @@ -157,30 +156,4 @@ %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } %td.actions %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } -======= - %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } - %td.bulk - %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } - %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} - %td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }} - %td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }} - %td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }} - %td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }} - %td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }} - %td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }} - %td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }} - %td.variant{ 'ng-show' => 'columns.variant.visible' } - %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.full_name }} - %td.quantity{ 'ng-show' => 'columns.quantity.visible' } - %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "quantity" } - %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} - %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } - %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'ofn-line-item-upd-attr' => "unit_value" } - %td.price{ 'ng-show' => 'columns.price.visible' } - %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-model' => "line_item.price", 'ng-readonly' => "true", 'ofn-line-item-upd-attr' => "price" } - %td.actions - %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } - %td.actions - %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } ->>>>>>> master %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' } From dbd81e60a1903f4391c1385d657c68650f889912 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 3 Jun 2015 16:18:09 +0800 Subject: [PATCH 53/56] Fixing customer index feature spec, only checking shop when something has been selected --- .../customers/controllers/customers_controller.js.coffee | 5 +++-- spec/features/admin/customers_spec.rb | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index be526a4349..c475f1e4df 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -9,8 +9,9 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, Customers tags: { name: "Tags", visible: true } $scope.$watch "shop", -> - Customers.loaded = false - $scope.customers = Customers.index(enterprise_id: $scope.shop.id) + if $scope.shop? + Customers.loaded = false + $scope.customers = Customers.index(enterprise_id: $scope.shop.id) $scope.loaded = -> Customers.loaded diff --git a/spec/features/admin/customers_spec.rb b/spec/features/admin/customers_spec.rb index b6c84e10cd..c84f8f35ed 100644 --- a/spec/features/admin/customers_spec.rb +++ b/spec/features/admin/customers_spec.rb @@ -24,7 +24,6 @@ feature 'Customers' do expect(page).to have_select2 "shop_id", with_options: [managed_distributor.name], without_options: [unmanaged_distributor.name] select2_select managed_distributor.name, from: "shop_id" - click_button "Go" # Loads the right customers expect(page).to have_selector "tr#c_#{customer1.id}" @@ -48,7 +47,6 @@ feature 'Customers' do it "allows updating of attributes", js: true do select2_select managed_distributor.name, from: "shop_id" - click_button "Go" within "tr#c_#{customer1.id}" do fill_in "code", with: "new-customer-code" From 359328a96e17d07a999169e185eb2c1b9cca5a17 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 13 May 2015 15:37:08 +1000 Subject: [PATCH 54/56] Producers and distributors lists include self where appropriate --- app/models/enterprise.rb | 8 ++++++-- spec/models/enterprise_spec.rb | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index f1e18f7cbe..4adfb05309 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -217,12 +217,16 @@ class Enterprise < ActiveRecord::Base ", self.id, self.id) end + def relatives_including_self + Enterprise.where(id: relatives.pluck(:id) | [id]) + end + def distributors - self.relatives.is_distributor + self.relatives_including_self.is_distributor end def suppliers - self.relatives.is_primary_producer + self.relatives_including_self.is_primary_producer end def website diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index df04324af2..388d80258e 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -121,14 +121,18 @@ describe Enterprise do e.relatives.should match_array [p, c] end + it "finds relatives_including_self" do + expect(e.relatives_including_self).to include e + end + it "scopes relatives to visible distributors" do - e.should_receive(:relatives).and_return(relatives = []) + e.should_receive(:relatives_including_self).and_return(relatives = []) relatives.should_receive(:is_distributor).and_return relatives e.distributors end it "scopes relatives to visible producers" do - e.should_receive(:relatives).and_return(relatives = []) + e.should_receive(:relatives_including_self).and_return(relatives = []) relatives.should_receive(:is_primary_producer).and_return relatives e.suppliers end From 3855ae133722477125ac3acbfd6d7f4ccbecb13f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 14 May 2015 13:04:16 +1000 Subject: [PATCH 55/56] Fixing bug on OC interface that prevented hubs from pulling their own products through --- .../order_cycle_permissions.rb | 8 ++++++-- .../order_cycle_permissions_spec.rb | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/open_food_network/order_cycle_permissions.rb b/lib/open_food_network/order_cycle_permissions.rb index 08fe7b2ee4..f728348630 100644 --- a/lib/open_food_network/order_cycle_permissions.rb +++ b/lib/open_food_network/order_cycle_permissions.rb @@ -128,6 +128,8 @@ module OpenFoodNetwork producers = related_enterprises_granting(:add_to_order_cycle, to: [hub], scope: Enterprise.is_primary_producer) permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers) + hub_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', hub) + # PLUS any variants that are already in an outgoing exchange of this hub, so things don't break # TODO: Remove this when all P-OC are sorted out active_variants = [] @@ -135,7 +137,7 @@ module OpenFoodNetwork active_variants = exchange.variants end - Spree::Variant.where(id: coordinator_variants | permitted_variants | active_variants) + Spree::Variant.where(id: coordinator_variants | hub_variants | permitted_variants | active_variants) else # Any variants produced by MY PRODUCERS that are in this order cycle, where my producer has granted P-OC to the hub producers = related_enterprises_granting(:add_to_order_cycle, to: [hub], scope: managed_participating_producers) @@ -165,6 +167,8 @@ module OpenFoodNetwork producers = related_enterprises_granting(:add_to_order_cycle, to: [hub], scope: Enterprise.is_primary_producer) permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers) + hub_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', hub) + # PLUS any variants that are already in an outgoing exchange of this hub, so things don't break # TODO: Remove this when all P-OC are sorted out active_variants = [] @@ -172,7 +176,7 @@ module OpenFoodNetwork active_variants = exchange.variants end - Spree::Variant.where(id: coordinator_variants | permitted_variants | active_variants) + Spree::Variant.where(id: coordinator_variants | hub_variants | permitted_variants | active_variants) else # Any of my managed producers in this order cycle granted P-OC by the hub granted_producers = related_enterprises_granted(:add_to_order_cycle, by: [hub], scope: managed_participating_producers) diff --git a/spec/lib/open_food_network/order_cycle_permissions_spec.rb b/spec/lib/open_food_network/order_cycle_permissions_spec.rb index d135a3500a..275d91d13d 100644 --- a/spec/lib/open_food_network/order_cycle_permissions_spec.rb +++ b/spec/lib/open_food_network/order_cycle_permissions_spec.rb @@ -545,6 +545,16 @@ module OpenFoodNetwork expect(visible).to_not include v2 end + context "where the hub produces products" do + # NOTE: No relationship to self required + let!(:v3) { create(:variant, product: create(:simple_product, supplier: hub)) } + + it "returns any variants produced by the hub" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v3 + end + end + # TODO: for backwards compatability, remove later context "when an exchange exists between the coordinator and the hub within this order cycle" do let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } @@ -712,6 +722,16 @@ module OpenFoodNetwork expect(visible).to_not include v2 end + context "where the hub produces products" do + # NOTE: No relationship to self required + let!(:v3) { create(:variant, product: create(:simple_product, supplier: hub)) } + + it "returns any variants produced by the hub" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v3 + end + end + # TODO: for backwards compatability, remove later context "when an exchange exists between the coordinator and the hub within this order cycle" do let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } From e93736b12386ac79bda101e756e730febee3ff40 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 3 Jun 2015 18:16:07 +0800 Subject: [PATCH 56/56] Ordering managed orders by id to fix spec --- app/controllers/spree/admin/orders_controller_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index f7d674048e..87c6f3329a 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -47,7 +47,7 @@ Spree::Admin::OrdersController.class_eval do def managed permissions = OpenFoodNetwork::Permissions.new(spree_current_user) - @orders = permissions.editable_orders.ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + @orders = permissions.editable_orders.order(:id).ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) render json: @orders, each_serializer: Api::Admin::OrderSerializer end end