From 30bb948d35f392cb59cd7983626cdbe03cd5997c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 15 Apr 2015 10:25:20 +1000 Subject: [PATCH 001/120] 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 002/120] 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 003/120] 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 004/120] 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 005/120] 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 006/120] 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 007/120] 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 008/120] 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 009/120] 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 010/120] 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 011/120] 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 012/120] 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 013/120] 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 014/120] 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 015/120] 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 016/120] 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 017/120] 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 018/120] 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 019/120] 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 020/120] 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 021/120] 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 022/120] 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 023/120] 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 024/120] 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 025/120] 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 026/120] 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 027/120] 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 028/120] 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 029/120] 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 030/120] 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 031/120] 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 032/120] 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 033/120] 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 034/120] 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 035/120] 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 036/120] 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 037/120] 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 038/120] 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 039/120] 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 040/120] 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 041/120] 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 042/120] 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 043/120] 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 044/120] 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 38e1bd413998b7eccf2d31716f9adc9085e4053e Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 1 May 2015 15:44:19 +1000 Subject: [PATCH 045/120] Fix indentation --- lib/open_food_network/order_and_distributor_report.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/open_food_network/order_and_distributor_report.rb b/lib/open_food_network/order_and_distributor_report.rb index 2662b176dd..011e8d19fe 100644 --- a/lib/open_food_network/order_and_distributor_report.rb +++ b/lib/open_food_network/order_and_distributor_report.rb @@ -1,4 +1,3 @@ - module OpenFoodNetwork class OrderAndDistributorReport @@ -8,14 +7,15 @@ module OpenFoodNetwork def header ["Order date", "Order Id", - "Customer Name","Customer Email", "Customer Phone", "Customer City", - "SKU", "Item name", "Variant", "Quantity", "Max Quantity", "Cost", "Shipping cost", - "Payment method", - "Distributor", "Distributor address", "Distributor city", "Distributor postcode", "Shipping instructions"] + "Customer Name","Customer Email", "Customer Phone", "Customer City", + "SKU", "Item name", "Variant", "Quantity", "Max Quantity", "Cost", "Shipping cost", + "Payment method", + "Distributor", "Distributor address", "Distributor city", "Distributor postcode", "Shipping instructions"] end def table order_and_distributor_details = [] + @orders.each do |order| order.line_items.each do |line_item| order_and_distributor_details << [order.created_at, order.id, @@ -25,6 +25,7 @@ module OpenFoodNetwork order.distributor.andand.name, order.distributor.address.address1, order.distributor.address.city, order.distributor.address.zipcode, order.special_instructions ] end end + order_and_distributor_details end end From 0a2f2e0fbaa5b393bc8b4ed1d15425784db7a111 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 7 May 2015 15:31:39 +1000 Subject: [PATCH 046/120] Output summary data without customisation Add route for xero invoices report Add require for reports controller decorator --- .../admin/reports_controller_decorator.rb | 11 +++- config/routes.rb | 1 + lib/open_food_network/xero_invoices_report.rb | 60 +++++++++++++++++++ spec/features/admin/reports_spec.rb | 60 +++++++++++++++++++ 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 lib/open_food_network/xero_invoices_report.rb diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 3aa4f560a1..3ef32aa074 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -7,6 +7,7 @@ require 'open_food_network/customers_report' require 'open_food_network/users_and_enterprises_report' require 'open_food_network/order_cycle_management_report' require 'open_food_network/sales_tax_report' +require 'open_food_network/xero_invoices_report' Spree::Admin::ReportsController.class_eval do @@ -679,7 +680,15 @@ Spree::Admin::ReportsController.class_eval do render_report(@report.header, @report.table, params[:csv], "users_and_enterprises_#{timestamp}.csv") end - def render_report (header, table, create_csv, csv_file_name) + def xero_invoices + @search = Spree::Order.complete.managed_by(spree_current_user).search(params[:q]) + orders = @search.result + @report = OpenFoodNetwork::XeroInvoicesReport.new orders + render_report(@report.header, @report.table, params[:csv], "xero_invoices_#{timestamp}.csv") + end + + + def render_report(header, table, create_csv, csv_file_name) unless create_csv render :html => table else diff --git a/config/routes.rb b/config/routes.rb index 4621ee4a35..66805807ce 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -134,6 +134,7 @@ Spree::Core::Engine.routes.prepend do match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management" match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post] match '/admin/reports/customers' => 'admin/reports#customers', :as => "customers_admin_reports", :via => [:get, :post] + match '/admin/reports/xero_invoices' => 'admin/reports#xero_invoices', :as => "xero_invoices_admin_reports", :via => [:get, :post] match '/admin', :to => 'admin/overview#index', :as => :admin match '/admin/payment_methods/show_provider_preferences' => 'admin/payment_methods#show_provider_preferences', :via => :get diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb new file mode 100644 index 0000000000..18c95073d3 --- /dev/null +++ b/lib/open_food_network/xero_invoices_report.rb @@ -0,0 +1,60 @@ +module OpenFoodNetwork + class XeroInvoicesReport + def initialize(orders) + @orders = orders + end + + def header + %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme) + end + + def table + rows = [] + + @orders.each do |order| + rows << summary_row(order, 'Total untaxable produce (no tax)', 0, 'GST Free Income') + rows << summary_row(order, 'Total taxable produce (tax inclusive)', 0, 'GST on Income') + rows << summary_row(order, 'Total untaxable fees (no tax)', 0, 'GST Free Income') + rows << summary_row(order, 'Total taxable fees (tax inclusive)', 0, 'GST on Income') + rows << summary_row(order, 'Delivery Shipping Cost (tax inclusive)', 0, 'Tax or No Tax - depending on enterprise setting') + end + + rows + end + + + private + + def summary_row(order, description, amount, tax_type) + [order.bill_address.full_name, + order.email, + order.bill_address.address1, + order.bill_address.address2, + '', + '', + order.bill_address.city, + order.bill_address.state, + order.bill_address.zipcode, + order.bill_address.country.andand.name, + order.number, # To customise + order.number, + Date.today, # To customise + 2.weeks.from_now.to_date, # To customise + '', + description, + '1', + amount, + '', + 'food sales', # To customise + tax_type, + '', + '', + '', + '', + Spree::Config.currency, + '' + ] + end + + end +end diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 309e0a3e81..70e7711759 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -298,4 +298,64 @@ feature %q{ ].sort end end + + describe "Xero invoices report" do + let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } + let(:distributor2) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } + let(:user1) { create_enterprise_user enterprises: [distributor1] } + let(:user2) { create_enterprise_user enterprises: [distributor2] } + let(:shipping_method) { create(:shipping_method, name: "Shipping", description: "Expensive", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 100.55)) } + let(:enterprise_fee) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.tax_category, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 120.0)) } + let(:order_cycle) { create(:simple_order_cycle, coordinator: distributor1, coordinator_fees: [enterprise_fee], distributors: [distributor1], variants: [product1.master]) } + + let!(:zone) { create(:zone_with_member) } + let(:country) { Spree::Country.find Spree::Config.default_country_id } + let(:bill_address) { create(:address, firstname: 'Customer', lastname: 'Name', address1: 'customer l1', address2: '', city: 'customer city', zipcode: 1234, country: country) } + let(:order1) { create(:order, order_cycle: order_cycle, distributor: user1.enterprises.first, shipping_method: shipping_method, bill_address: bill_address) } + let(:product1) { create(:taxed_product, zone: zone, price: 12.54, tax_rate_amount: 0) } + let(:product2) { create(:taxed_product, zone: zone, price: 500.15, tax_rate_amount: 0.2) } + + let!(:line_item1) { create(:line_item, variant: product1.master, price: 12.54, quantity: 1, order: order1) } + let!(:line_item2) { create(:line_item, variant: product2.master, price: 500.15, quantity: 3, order: order1) } + + let!(:adj_shipping) { create(:adjustment, adjustable: order1, label: "Shipping", amount: 100.55) } + + before do + order1.update_attribute :email, 'customer@email.com' + Timecop.travel(Time.zone.local(2015, 4, 25, 14, 0, 0)) { order1.finalize! } + + login_to_admin_section + click_link 'Reports' + + click_link 'Xero invoices' + end + + around do |example| + Timecop.travel(Time.zone.local(2015, 4, 26, 14, 0, 0)) do + example.yield + end + end + + it "shows Xero invoices report" do + rows = find("table#listing_invoices").all("tr") + table = rows.map { |r| r.all("th,td").map { |c| c.text.strip } } + + table.should == [ + %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme), + xero_invoice_row('Total untaxable produce (no tax)', 0, 'GST Free Income'), + xero_invoice_row('Total taxable produce (tax inclusive)', 0, 'GST on Income'), + xero_invoice_row('Total untaxable fees (no tax)', 0, 'GST Free Income'), + xero_invoice_row('Total taxable fees (tax inclusive)', 0, 'GST on Income'), + xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 0, 'Tax or No Tax - depending on enterprise setting') + ] + end + + + private + + def xero_invoice_row(description, amount, tax_type) + ['Customer Name', 'customer@email.com', 'customer l1', '', '', '', 'customer city', 'Victoria', '1234', country.name, order1.number, order1.number, '2015-04-26', '2015-05-10', '', description, '1', amount.to_s, '', 'food sales', tax_type, '', '', '', '', Spree::Config.currency, ''] + + end + end end From 0737ac8da02f253a1c7a5e766240d90090c9c87b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 14 May 2015 12:11:38 +1000 Subject: [PATCH 047/120] Write an rspec table matcher that gives informative error messages --- spec/support/matchers/table_matchers.rb | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/spec/support/matchers/table_matchers.rb b/spec/support/matchers/table_matchers.rb index 053562b9e4..411b0b646f 100644 --- a/spec/support/matchers/table_matchers.rb +++ b/spec/support/matchers/table_matchers.rb @@ -26,3 +26,44 @@ RSpec::Matchers.define :have_table_row do |row| node.all('tr').map { |tr| tr.all('th, td').map(&:text) } end end + + + +# find("#my-table").should match_table [[...]] +RSpec::Matchers.define :match_table do |expected_table| + + match_for_should do |node| + rows = node. + all("tr"). + map { |r| r.all("th,td").map { |c| c.text.strip } } + + if rows.count != expected_table.count + @failure_message = "found table with #{rows.count} rows, expected #{expected_table.count}" + + else + rows.each_with_index do |row, i| + expected_row = expected_table[i] + if row.count != expected_row.count + @failure_message = "row #{i} has #{row.count} columns, expected #{expected_row.count}" + break + + elsif row != expected_row + row.each_with_index do |cell, j| + if cell != expected_row[j] + @failure_message = "cell [#{i}, #{j}] has content '#{cell}', expected '#{expected_row[j]}'" + break + end + end + break if @failure_message + end + end + end + + @failure_message.nil? + end + + failure_message_for_should do |text| + @failure_message + end + +end From c5b618b1f4589f8243da2455e509fc012c2ebc09 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 14 May 2015 12:25:48 +1000 Subject: [PATCH 048/120] Admin can customise some fields on Xero invoices report Add require for xero invoices report spec --- .../admin/reports_controller_decorator.rb | 6 ++- .../admin/reports/xero_invoices.html.haml | 33 +++++++++++++++ lib/open_food_network/xero_invoices_report.rb | 32 +++++++++------ spec/features/admin/reports_spec.rb | 40 +++++++++++++++---- .../xero_invoices_report_spec.rb | 25 ++++++++++++ 5 files changed, 115 insertions(+), 21 deletions(-) create mode 100644 app/views/spree/admin/reports/xero_invoices.html.haml create mode 100644 spec/lib/open_food_network/xero_invoices_report_spec.rb diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 3ef32aa074..15fab60d89 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -683,7 +683,7 @@ Spree::Admin::ReportsController.class_eval do def xero_invoices @search = Spree::Order.complete.managed_by(spree_current_user).search(params[:q]) orders = @search.result - @report = OpenFoodNetwork::XeroInvoicesReport.new orders + @report = OpenFoodNetwork::XeroInvoicesReport.new orders, params render_report(@report.header, @report.table, params[:csv], "xero_invoices_#{timestamp}.csv") end @@ -725,7 +725,9 @@ Spree::Admin::ReportsController.class_eval do :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" }, :users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" }, :order_cycle_management => {:name => "Order Cycle Management", :description => ''}, - :sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" } + :sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" }, + :xero_invoices => { :name => "Xero Invoices", :description => 'Invoices for import into Xero' } + } # Return only reports the user is authorized to view. reports.select { |action| can? action, :report } diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml new file mode 100644 index 0000000000..8c11e5f543 --- /dev/null +++ b/app/views/spree/admin/reports/xero_invoices.html.haml @@ -0,0 +1,33 @@ += form_tag spree.xero_invoices_admin_reports_path do + .row + .four.columns.alpha= label_tag :initial_invoice_number, "Initial invoice number:" + .twelve.columns.omega= text_field_tag :initial_invoice_number + .row + .four.columns.alpha= label_tag :invoice_date, "Invoice date:" + .twelve.columns.omega= text_field_tag :invoice_date, '', class: 'datetimepicker' + .row + .four.columns.alpha= label_tag :due_date, "Due date:" + .twelve.columns.omega= text_field_tag :due_date, '', class: 'datetimepicker' + .row + .four.columns.alpha= label_tag :account_code, "Account code:" + .twelve.columns.omega= text_field_tag :account_code + .row + .four.columns.alpha= label_tag :csv, "Download as CSV:" + .twelve.columns.omega= check_box_tag :csv + .row + .four.columns.alpha= button t(:search) + + +%table#listing_invoices.index + %thead + %tr + - @report.header.each do |header| + %th= header + %tbody + - @report.table.each do |row| + %tr + - row.each do |column| + %td= column + - if @report.table.empty? + %tr + %td{:colspan => "2"}= t(:none) diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 18c95073d3..784550a4b4 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -1,7 +1,10 @@ module OpenFoodNetwork class XeroInvoicesReport - def initialize(orders) + def initialize(orders, opts={}) @orders = orders + @opts = opts.reverse_merge({invoice_date: Date.today, + due_date: 2.weeks.from_now.to_date, + account_code: 'food sales'}) end def header @@ -11,12 +14,13 @@ module OpenFoodNetwork def table rows = [] - @orders.each do |order| - rows << summary_row(order, 'Total untaxable produce (no tax)', 0, 'GST Free Income') - rows << summary_row(order, 'Total taxable produce (tax inclusive)', 0, 'GST on Income') - rows << summary_row(order, 'Total untaxable fees (no tax)', 0, 'GST Free Income') - rows << summary_row(order, 'Total taxable fees (tax inclusive)', 0, 'GST on Income') - rows << summary_row(order, 'Delivery Shipping Cost (tax inclusive)', 0, 'Tax or No Tax - depending on enterprise setting') + @orders.each_with_index do |order, i| + invoice_number = invoice_number_for(order, i) + rows << summary_row(order, 'Total untaxable produce (no tax)', 0, invoice_number, 'GST Free Income', @opts) + rows << summary_row(order, 'Total taxable produce (tax inclusive)', 0, invoice_number, 'GST on Income', @opts) + rows << summary_row(order, 'Total untaxable fees (no tax)', 0, invoice_number, 'GST Free Income', @opts) + rows << summary_row(order, 'Total taxable fees (tax inclusive)', 0, invoice_number, 'GST on Income', @opts) + rows << summary_row(order, 'Delivery Shipping Cost (tax inclusive)', 0, invoice_number, 'Tax or No Tax - depending on enterprise setting', @opts) end rows @@ -25,7 +29,11 @@ module OpenFoodNetwork private - def summary_row(order, description, amount, tax_type) + def invoice_number_for(order, i) + @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number + end + + def summary_row(order, description, amount, invoice_number, tax_type, opts={}) [order.bill_address.full_name, order.email, order.bill_address.address1, @@ -36,16 +44,16 @@ module OpenFoodNetwork order.bill_address.state, order.bill_address.zipcode, order.bill_address.country.andand.name, - order.number, # To customise + invoice_number, order.number, - Date.today, # To customise - 2.weeks.from_now.to_date, # To customise + opts[:invoice_date], + opts[:due_date], '', description, '1', amount, '', - 'food sales', # To customise + opts[:account_code], tax_type, '', '', diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 70e7711759..49a0979406 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -327,7 +327,7 @@ feature %q{ login_to_admin_section click_link 'Reports' - click_link 'Xero invoices' + click_link 'Xero Invoices' end around do |example| @@ -337,10 +337,7 @@ feature %q{ end it "shows Xero invoices report" do - rows = find("table#listing_invoices").all("tr") - table = rows.map { |r| r.all("th,td").map { |c| c.text.strip } } - - table.should == [ + xero_invoice_table.should match_table [ %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme), xero_invoice_row('Total untaxable produce (no tax)', 0, 'GST Free Income'), xero_invoice_row('Total taxable produce (tax inclusive)', 0, 'GST on Income'), @@ -350,11 +347,40 @@ feature %q{ ] end + it "can customise a number of fields" do + fill_in 'initial_invoice_number', with: '5' + fill_in 'invoice_date', with: '2015-02-12' + fill_in 'due_date', with: '2015-03-12' + fill_in 'account_code', with: 'abc123' + click_button 'Search' + + opts = {invoice_number: '5', invoice_date: '2015-02-12', due_date: '2015-03-12', account_code: 'abc123'} + + xero_invoice_table.should match_table [ + %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme), + xero_invoice_row('Total untaxable produce (no tax)', 0, 'GST Free Income', opts), + xero_invoice_row('Total taxable produce (tax inclusive)', 0, 'GST on Income', opts), + xero_invoice_row('Total untaxable fees (no tax)', 0, 'GST Free Income', opts), + xero_invoice_row('Total taxable fees (tax inclusive)', 0, 'GST on Income', opts), + xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 0, 'Tax or No Tax - depending on enterprise setting', opts) + ] + + # TODO: + # - Amounts + # - Tax specification for shipping + end + private - def xero_invoice_row(description, amount, tax_type) - ['Customer Name', 'customer@email.com', 'customer l1', '', '', '', 'customer city', 'Victoria', '1234', country.name, order1.number, order1.number, '2015-04-26', '2015-05-10', '', description, '1', amount.to_s, '', 'food sales', tax_type, '', '', '', '', Spree::Config.currency, ''] + def xero_invoice_table + find("table#listing_invoices") + end + + def xero_invoice_row(description, amount, tax_type, opts={}) + opts.reverse_merge!({invoice_number: order1.number, invoice_date: '2015-04-26', due_date: '2015-05-10', account_code: 'food sales'}) + + ['Customer Name', 'customer@email.com', 'customer l1', '', '', '', 'customer city', 'Victoria', '1234', country.name, opts[:invoice_number], order1.number, opts[:invoice_date], opts[:due_date], '', description, '1', amount.to_s, '', opts[:account_code], tax_type, '', '', '', '', Spree::Config.currency, ''] end end diff --git a/spec/lib/open_food_network/xero_invoices_report_spec.rb b/spec/lib/open_food_network/xero_invoices_report_spec.rb new file mode 100644 index 0000000000..9756629f0e --- /dev/null +++ b/spec/lib/open_food_network/xero_invoices_report_spec.rb @@ -0,0 +1,25 @@ +require 'open_food_network/xero_invoices_report' + +module OpenFoodNetwork + describe XeroInvoicesReport do + subject { XeroInvoicesReport.new [] } + + describe "generating invoice numbers" do + let(:order) { double(:order, number: 'R731032860') } + + describe "when no initial invoice number is given" do + it "returns the order number" do + subject.send(:invoice_number_for, order, 123).should == 'R731032860' + end + end + + describe "when an initial invoice number is given" do + subject { XeroInvoicesReport.new [], {initial_invoice_number: '123'} } + + it "increments the number by the index" do + subject.send(:invoice_number_for, order, 456).should == 579 + end + end + end + end +end From 5660e3737e2e5fd82b01fb2dcba7daf22e305704 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 14 May 2015 13:01:46 +1000 Subject: [PATCH 049/120] Extract order rows generation to method --- lib/open_food_network/xero_invoices_report.rb | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 784550a4b4..7308f7740b 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -16,11 +16,7 @@ module OpenFoodNetwork @orders.each_with_index do |order, i| invoice_number = invoice_number_for(order, i) - rows << summary_row(order, 'Total untaxable produce (no tax)', 0, invoice_number, 'GST Free Income', @opts) - rows << summary_row(order, 'Total taxable produce (tax inclusive)', 0, invoice_number, 'GST on Income', @opts) - rows << summary_row(order, 'Total untaxable fees (no tax)', 0, invoice_number, 'GST Free Income', @opts) - rows << summary_row(order, 'Total taxable fees (tax inclusive)', 0, invoice_number, 'GST on Income', @opts) - rows << summary_row(order, 'Delivery Shipping Cost (tax inclusive)', 0, invoice_number, 'Tax or No Tax - depending on enterprise setting', @opts) + rows += rows_for_order(order, invoice_number, @opts) end rows @@ -29,6 +25,17 @@ module OpenFoodNetwork private + def rows_for_order(order, invoice_number, opts) + [ + summary_row(order, 'Total untaxable produce (no tax)', 0, invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable produce (tax inclusive)', 0, invoice_number, 'GST on Income', opts), + summary_row(order, 'Total untaxable fees (no tax)', 0, invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable fees (tax inclusive)', 0, invoice_number, 'GST on Income', opts), + summary_row(order, 'Delivery Shipping Cost (tax inclusive)', 0, invoice_number, 'Tax or No Tax - depending on enterprise setting', opts) + ] + end + + def invoice_number_for(order, i) @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number end From ca1d88d8b142d015edbc71e8a9de961d3db564d8 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 May 2015 11:49:24 +1000 Subject: [PATCH 050/120] Find line items with and without tax --- app/models/spree/line_item_decorator.rb | 9 +++++++++ spec/models/spree/line_item_spec.rb | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index b2c0a583fc..0d6532327c 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -23,6 +23,15 @@ Spree::LineItem.class_eval do where('spree_products.supplier_id IN (?)', enterprises) } + scope :with_tax, joins(:adjustments). + where('spree_adjustments.originator_type = ?', 'Spree::TaxRate'). + select('DISTINCT spree_line_items.*') + + # Line items without a Spree::TaxRate-originated adjustment + scope :without_tax, joins("LEFT OUTER JOIN spree_adjustments ON (spree_adjustments.adjustable_id=spree_line_items.id AND spree_adjustments.adjustable_type = 'Spree::LineItem' AND spree_adjustments.originator_type='Spree::TaxRate')"). + where('spree_adjustments.id IS NULL') + + def price_with_adjustments # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, # so line_item.adjustments returns an empty array diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb index 6166732b76..b39b51e1a5 100644 --- a/spec/models/spree/line_item_spec.rb +++ b/spec/models/spree/line_item_spec.rb @@ -24,6 +24,22 @@ module Spree LineItem.supplied_by_any([s2]).should == [li2] LineItem.supplied_by_any([s1, s2]).sort.should == [li1, li2].sort end + + describe "finding line items with and without tax" do + let(:tax_rate) { create(:tax_rate, calculator: Spree::Calculator::DefaultTax.new) } + let!(:adjustment1) { create(:adjustment, adjustable: li1, originator: tax_rate, label: "TR", amount: 123, included_tax: 10.00) } + let!(:adjustment2) { create(:adjustment, adjustable: li1, originator: tax_rate, label: "TR", amount: 123, included_tax: 10.00) } + + before { li1; li2 } + + it "finds line items with tax" do + LineItem.with_tax.should == [li1] + end + + it "finds line items without tax" do + LineItem.without_tax.should == [li2] + end + end end describe "calculating price with adjustments" do From 0dcd8eb8cc9df25af785b09a26cb21bc5edbd2f8 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 May 2015 11:57:01 +1000 Subject: [PATCH 051/120] Find adjustments with and without tax --- app/models/spree/adjustment_decorator.rb | 2 ++ spec/models/spree/adjustment_spec.rb | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb index 836080183c..153cc10a82 100644 --- a/app/models/spree/adjustment_decorator.rb +++ b/app/models/spree/adjustment_decorator.rb @@ -4,6 +4,8 @@ module Spree scope :enterprise_fee, where(originator_type: 'EnterpriseFee') scope :included_tax, where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem') + scope :with_tax, where('spree_adjustments.included_tax > 0') + scope :without_tax, where('spree_adjustments.included_tax = 0') attr_accessible :included_tax diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index 579965aa7a..bd952f2e9c 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -5,6 +5,21 @@ module Spree adjustment.metadata.should be end + describe "finding adjustments with and without tax included" do + let!(:adjustment_with_tax) { create(:adjustment, included_tax: 123) } + let!(:adjustment_without_tax) { create(:adjustment, included_tax: 0) } + + it "finds adjustments with tax" do + Adjustment.with_tax.should include adjustment_with_tax + Adjustment.with_tax.should_not include adjustment_without_tax + end + + it "finds adjustments without tax" do + Adjustment.without_tax.should include adjustment_without_tax + Adjustment.without_tax.should_not include adjustment_with_tax + end + end + describe "recording included tax" do describe "TaxRate adjustments" do let!(:zone) { create(:zone_with_member) } From dc8270ed72971a490f444fdc6d06f491d8022706 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 May 2015 12:16:35 +1000 Subject: [PATCH 052/120] Display amounts on xero invoice report --- lib/open_food_network/xero_invoices_report.rb | 30 +++++++++++++++--- spec/features/admin/reports_spec.rb | 31 ++++++++++--------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 7308f7740b..5b346f9263 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -27,15 +27,35 @@ module OpenFoodNetwork def rows_for_order(order, invoice_number, opts) [ - summary_row(order, 'Total untaxable produce (no tax)', 0, invoice_number, 'GST Free Income', opts), - summary_row(order, 'Total taxable produce (tax inclusive)', 0, invoice_number, 'GST on Income', opts), - summary_row(order, 'Total untaxable fees (no tax)', 0, invoice_number, 'GST Free Income', opts), - summary_row(order, 'Total taxable fees (tax inclusive)', 0, invoice_number, 'GST on Income', opts), - summary_row(order, 'Delivery Shipping Cost (tax inclusive)', 0, invoice_number, 'Tax or No Tax - depending on enterprise setting', opts) + summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable produce (tax inclusive)', total_taxable_products(order), invoice_number, 'GST on Income', opts), + summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts), + summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, 'Tax or No Tax - depending on enterprise setting', opts) ] end + def total_untaxable_products(order) + order.line_items.without_tax.sum &:amount + end + + def total_taxable_products(order) + order.line_items.with_tax.sum &:amount + end + + def total_untaxable_fees(order) + order.adjustments.enterprise_fee.without_tax.sum &:amount + end + + def total_taxable_fees(order) + order.adjustments.enterprise_fee.with_tax.sum &:amount + end + + def total_shipping(order) + order.adjustments.shipping.sum &:amount + end + def invoice_number_for(order, i) @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number end diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 49a0979406..2de44d175d 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -305,8 +305,9 @@ feature %q{ let(:user1) { create_enterprise_user enterprises: [distributor1] } let(:user2) { create_enterprise_user enterprises: [distributor2] } let(:shipping_method) { create(:shipping_method, name: "Shipping", description: "Expensive", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 100.55)) } - let(:enterprise_fee) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.tax_category, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 120.0)) } - let(:order_cycle) { create(:simple_order_cycle, coordinator: distributor1, coordinator_fees: [enterprise_fee], distributors: [distributor1], variants: [product1.master]) } + let(:enterprise_fee1) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.tax_category, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 10)) } + let(:enterprise_fee2) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.tax_category, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 20)) } + let(:order_cycle) { create(:simple_order_cycle, coordinator: distributor1, coordinator_fees: [enterprise_fee1, enterprise_fee2], distributors: [distributor1], variants: [product1.master]) } let!(:zone) { create(:zone_with_member) } let(:country) { Spree::Country.find Spree::Config.default_country_id } @@ -318,7 +319,10 @@ feature %q{ let!(:line_item1) { create(:line_item, variant: product1.master, price: 12.54, quantity: 1, order: order1) } let!(:line_item2) { create(:line_item, variant: product2.master, price: 500.15, quantity: 3, order: order1) } - let!(:adj_shipping) { create(:adjustment, adjustable: order1, label: "Shipping", amount: 100.55) } + let!(:adj_shipping) { create(:adjustment, adjustable: order1, label: "Shipping", originator: shipping_method, amount: 100.55) } + let!(:adj_fee1) { create(:adjustment, adjustable: order1, originator: enterprise_fee1, label: "Enterprise fee untaxed", amount: 10, included_tax: 0) } + let!(:adj_fee2) { create(:adjustment, adjustable: order1, originator: enterprise_fee2, label: "Enterprise fee taxed", amount: 20, included_tax: 2) } + before do order1.update_attribute :email, 'customer@email.com' @@ -339,11 +343,11 @@ feature %q{ it "shows Xero invoices report" do xero_invoice_table.should match_table [ %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme), - xero_invoice_row('Total untaxable produce (no tax)', 0, 'GST Free Income'), - xero_invoice_row('Total taxable produce (tax inclusive)', 0, 'GST on Income'), - xero_invoice_row('Total untaxable fees (no tax)', 0, 'GST Free Income'), - xero_invoice_row('Total taxable fees (tax inclusive)', 0, 'GST on Income'), - xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 0, 'Tax or No Tax - depending on enterprise setting') + xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income'), + xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income'), + xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income'), + xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income'), + xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'Tax or No Tax - depending on enterprise setting') ] end @@ -358,15 +362,14 @@ feature %q{ xero_invoice_table.should match_table [ %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme), - xero_invoice_row('Total untaxable produce (no tax)', 0, 'GST Free Income', opts), - xero_invoice_row('Total taxable produce (tax inclusive)', 0, 'GST on Income', opts), - xero_invoice_row('Total untaxable fees (no tax)', 0, 'GST Free Income', opts), - xero_invoice_row('Total taxable fees (tax inclusive)', 0, 'GST on Income', opts), - xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 0, 'Tax or No Tax - depending on enterprise setting', opts) + xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income', opts), + xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income', opts), + xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts), + xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income', opts), + xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'Tax or No Tax - depending on enterprise setting', opts) ] # TODO: - # - Amounts # - Tax specification for shipping end From ca37efdd262277c3b7fbabe9f3f07847c06395fb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 May 2015 12:44:48 +1000 Subject: [PATCH 053/120] Display whether there is tax included in shipping --- lib/open_food_network/xero_invoices_report.rb | 15 ++++++++++----- spec/features/admin/reports_spec.rb | 9 +++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 5b346f9263..504bf03349 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -27,11 +27,11 @@ module OpenFoodNetwork def rows_for_order(order, invoice_number, opts) [ - summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts), - summary_row(order, 'Total taxable produce (tax inclusive)', total_taxable_products(order), invoice_number, 'GST on Income', opts), - summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts), - summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts), - summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, 'Tax or No Tax - depending on enterprise setting', opts) + summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable produce (tax inclusive)', total_taxable_products(order), invoice_number, 'GST on Income', opts), + summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts), + summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, tax_on_shipping_s(order), opts) ] end @@ -56,6 +56,11 @@ module OpenFoodNetwork order.adjustments.shipping.sum &:amount end + def tax_on_shipping_s(order) + tax_on_shipping = order.adjustments.shipping.sum(&:included_tax) > 0 + tax_on_shipping ? 'GST on Income' : 'GST Free Income' + end + def invoice_number_for(order, i) @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number end diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 2de44d175d..86cf803fe5 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -319,7 +319,7 @@ feature %q{ let!(:line_item1) { create(:line_item, variant: product1.master, price: 12.54, quantity: 1, order: order1) } let!(:line_item2) { create(:line_item, variant: product2.master, price: 500.15, quantity: 3, order: order1) } - let!(:adj_shipping) { create(:adjustment, adjustable: order1, label: "Shipping", originator: shipping_method, amount: 100.55) } + let!(:adj_shipping) { create(:adjustment, adjustable: order1, label: "Shipping", originator: shipping_method, amount: 100.55, included_tax: 10.06) } let!(:adj_fee1) { create(:adjustment, adjustable: order1, originator: enterprise_fee1, label: "Enterprise fee untaxed", amount: 10, included_tax: 0) } let!(:adj_fee2) { create(:adjustment, adjustable: order1, originator: enterprise_fee2, label: "Enterprise fee taxed", amount: 20, included_tax: 2) } @@ -347,7 +347,7 @@ feature %q{ xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income'), xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income'), xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income'), - xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'Tax or No Tax - depending on enterprise setting') + xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income') ] end @@ -366,11 +366,8 @@ feature %q{ xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income', opts), xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts), xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income', opts), - xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'Tax or No Tax - depending on enterprise setting', opts) + xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income', opts) ] - - # TODO: - # - Tax specification for shipping end From 3640a71ab8e7b5a7c7dab1bfa8016f5faa981e7e Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 May 2015 15:05:18 +1000 Subject: [PATCH 054/120] Reorder methods --- lib/open_food_network/xero_invoices_report.rb | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 504bf03349..9a316e08ed 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -25,6 +25,10 @@ module OpenFoodNetwork private + def invoice_number_for(order, i) + @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number + end + def rows_for_order(order, invoice_number, opts) [ summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts), @@ -35,36 +39,6 @@ module OpenFoodNetwork ] end - - def total_untaxable_products(order) - order.line_items.without_tax.sum &:amount - end - - def total_taxable_products(order) - order.line_items.with_tax.sum &:amount - end - - def total_untaxable_fees(order) - order.adjustments.enterprise_fee.without_tax.sum &:amount - end - - def total_taxable_fees(order) - order.adjustments.enterprise_fee.with_tax.sum &:amount - end - - def total_shipping(order) - order.adjustments.shipping.sum &:amount - end - - def tax_on_shipping_s(order) - tax_on_shipping = order.adjustments.shipping.sum(&:included_tax) > 0 - tax_on_shipping ? 'GST on Income' : 'GST Free Income' - end - - def invoice_number_for(order, i) - @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number - end - def summary_row(order, description, amount, invoice_number, tax_type, opts={}) [order.bill_address.full_name, order.email, @@ -96,5 +70,29 @@ module OpenFoodNetwork ] end + def total_untaxable_products(order) + order.line_items.without_tax.sum &:amount + end + + def total_taxable_products(order) + order.line_items.with_tax.sum &:amount + end + + def total_untaxable_fees(order) + order.adjustments.enterprise_fee.without_tax.sum &:amount + end + + def total_taxable_fees(order) + order.adjustments.enterprise_fee.with_tax.sum &:amount + end + + def total_shipping(order) + order.adjustments.shipping.sum &:amount + end + + def tax_on_shipping_s(order) + tax_on_shipping = order.adjustments.shipping.sum(&:included_tax) > 0 + tax_on_shipping ? 'GST on Income' : 'GST Free Income' + end end end From 3d4a0f84077fd58afdbddfcbdea85efef267882d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 May 2015 15:27:46 +1000 Subject: [PATCH 055/120] Xero invoices report: Preserve form fields on submit, do not show rows with no cost --- .../spree/admin/reports_controller_decorator.rb | 2 +- app/views/spree/admin/reports/xero_invoices.html.haml | 8 ++++---- lib/open_food_network/xero_invoices_report.rb | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 15fab60d89..df86d224fd 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -681,7 +681,7 @@ Spree::Admin::ReportsController.class_eval do end def xero_invoices - @search = Spree::Order.complete.managed_by(spree_current_user).search(params[:q]) + @search = Spree::Order.complete.managed_by(spree_current_user).order('id DESC').search(params[:q]) orders = @search.result @report = OpenFoodNetwork::XeroInvoicesReport.new orders, params render_report(@report.header, @report.table, params[:csv], "xero_invoices_#{timestamp}.csv") diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml index 8c11e5f543..9433ee91bb 100644 --- a/app/views/spree/admin/reports/xero_invoices.html.haml +++ b/app/views/spree/admin/reports/xero_invoices.html.haml @@ -1,16 +1,16 @@ = form_tag spree.xero_invoices_admin_reports_path do .row .four.columns.alpha= label_tag :initial_invoice_number, "Initial invoice number:" - .twelve.columns.omega= text_field_tag :initial_invoice_number + .twelve.columns.omega= text_field_tag :initial_invoice_number, params[:initial_invoice_number] .row .four.columns.alpha= label_tag :invoice_date, "Invoice date:" - .twelve.columns.omega= text_field_tag :invoice_date, '', class: 'datetimepicker' + .twelve.columns.omega= text_field_tag :invoice_date, params[:invoice_date], class: 'datetimepicker' .row .four.columns.alpha= label_tag :due_date, "Due date:" - .twelve.columns.omega= text_field_tag :due_date, '', class: 'datetimepicker' + .twelve.columns.omega= text_field_tag :due_date, params[:due_date], class: 'datetimepicker' .row .four.columns.alpha= label_tag :account_code, "Account code:" - .twelve.columns.omega= text_field_tag :account_code + .twelve.columns.omega= text_field_tag :account_code, params[:account_code] .row .four.columns.alpha= label_tag :csv, "Download as CSV:" .twelve.columns.omega= check_box_tag :csv diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 9a316e08ed..b3d2bc77f3 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -36,10 +36,12 @@ module OpenFoodNetwork summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts), summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts), summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, tax_on_shipping_s(order), opts) - ] + ].compact end def summary_row(order, description, amount, invoice_number, tax_type, opts={}) + return nil if amount == 0 + [order.bill_address.full_name, order.email, order.bill_address.address1, From a467d3c379f3b2f60b982fae662f05ee1df0cfcb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 May 2015 16:20:56 +1000 Subject: [PATCH 056/120] Add filtering to xero invoices report - order date range, hub and order cycle --- .../spree/admin/reports_controller_decorator.rb | 7 +++++++ .../spree/admin/reports/xero_invoices.html.haml | 13 ++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index df86d224fd..9633b6cd68 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -681,6 +681,13 @@ Spree::Admin::ReportsController.class_eval do end def xero_invoices + if request.get? + params[:q] ||= {} + params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month + end + @distributors = Enterprise.is_distributor.managed_by(spree_current_user) + @order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC') + @search = Spree::Order.complete.managed_by(spree_current_user).order('id DESC').search(params[:q]) orders = @search.result @report = OpenFoodNetwork::XeroInvoicesReport.new orders, params diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml index 9433ee91bb..1ae4e3b279 100644 --- a/app/views/spree/admin/reports/xero_invoices.html.haml +++ b/app/views/spree/admin/reports/xero_invoices.html.haml @@ -1,4 +1,15 @@ -= form_tag spree.xero_invoices_admin_reports_path do += form_for @search, url: spree.xero_invoices_admin_reports_path do |f| + = render 'date_range_form', f: f + + .row + .four.columns.alpha= label_tag nil, "Hub: " + .four.columns.omega= f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) + .row + .four.columns.alpha= label_tag nil, "Order Cycle: " + .four.columns.omega= f.select(:order_cycle_id_eq, + options_for_select(report_order_cycle_options(@order_cycles), params[:q][:order_cycle_id_eq]), + {:include_blank => true}, {:class => "select2 fullwidth"}) + .row .four.columns.alpha= label_tag :initial_invoice_number, "Initial invoice number:" .twelve.columns.omega= text_field_tag :initial_invoice_number, params[:initial_invoice_number] From f7642b2a1b8e6fb19cff2a639f1a44c6f360cc09 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 May 2015 16:51:04 +1000 Subject: [PATCH 057/120] When blank values are submitted, do not override defaults --- lib/open_food_network/xero_invoices_report.rb | 9 ++++++--- .../open_food_network/xero_invoices_report_spec.rb | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index b3d2bc77f3..79305d655a 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -2,9 +2,12 @@ module OpenFoodNetwork class XeroInvoicesReport def initialize(orders, opts={}) @orders = orders - @opts = opts.reverse_merge({invoice_date: Date.today, - due_date: 2.weeks.from_now.to_date, - account_code: 'food sales'}) + + @opts = opts. + reject { |k, v| v.blank? }. + reverse_merge({invoice_date: Date.today, + due_date: 2.weeks.from_now.to_date, + account_code: 'food sales'}) end def header diff --git a/spec/lib/open_food_network/xero_invoices_report_spec.rb b/spec/lib/open_food_network/xero_invoices_report_spec.rb index 9756629f0e..8551d663a8 100644 --- a/spec/lib/open_food_network/xero_invoices_report_spec.rb +++ b/spec/lib/open_food_network/xero_invoices_report_spec.rb @@ -4,6 +4,18 @@ module OpenFoodNetwork describe XeroInvoicesReport do subject { XeroInvoicesReport.new [] } + describe "option defaults" do + let(:report) { XeroInvoicesReport.new [], {initial_invoice_number: '', invoice_date: '', due_date: '', account_code: ''} } + + around { |example| Timecop.travel(Time.zone.local(2015, 5, 5, 14, 0, 0)) { example.run } } + + it "uses defaults when blank params are passed" do + report.instance_variable_get(:@opts).should == {invoice_date: Date.civil(2015, 5, 5), + due_date: Date.civil(2015, 5, 19), + account_code: 'food sales'} + end + end + describe "generating invoice numbers" do let(:order) { double(:order, number: 'R731032860') } From 70b5ac4785dab82d79853ad43fc48c61dab11c40 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 May 2015 16:55:52 +1000 Subject: [PATCH 058/120] Add column for whether the order has been paid for --- lib/open_food_network/xero_invoices_report.rb | 5 +++-- spec/features/admin/reports_spec.rb | 10 +++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 79305d655a..1a7f7bd636 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -11,7 +11,7 @@ module OpenFoodNetwork end def header - %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme) + %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme Paid?) end def table @@ -71,7 +71,8 @@ module OpenFoodNetwork '', '', Spree::Config.currency, - '' + '', + order.paid? ? 'Y' : 'N' ] end diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 86cf803fe5..d8f7e1e14d 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -342,7 +342,7 @@ feature %q{ it "shows Xero invoices report" do xero_invoice_table.should match_table [ - %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme), + xero_invoice_header, xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income'), xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income'), xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income'), @@ -361,7 +361,7 @@ feature %q{ opts = {invoice_number: '5', invoice_date: '2015-02-12', due_date: '2015-03-12', account_code: 'abc123'} xero_invoice_table.should match_table [ - %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme), + xero_invoice_header, xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income', opts), xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income', opts), xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts), @@ -377,10 +377,14 @@ feature %q{ find("table#listing_invoices") end + def xero_invoice_header + %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme Paid?) + end + def xero_invoice_row(description, amount, tax_type, opts={}) opts.reverse_merge!({invoice_number: order1.number, invoice_date: '2015-04-26', due_date: '2015-05-10', account_code: 'food sales'}) - ['Customer Name', 'customer@email.com', 'customer l1', '', '', '', 'customer city', 'Victoria', '1234', country.name, opts[:invoice_number], order1.number, opts[:invoice_date], opts[:due_date], '', description, '1', amount.to_s, '', opts[:account_code], tax_type, '', '', '', '', Spree::Config.currency, ''] + ['Customer Name', 'customer@email.com', 'customer l1', '', '', '', 'customer city', 'Victoria', '1234', country.name, opts[:invoice_number], order1.number, opts[:invoice_date], opts[:due_date], '', description, '1', amount.to_s, '', opts[:account_code], tax_type, '', '', '', '', Spree::Config.currency, '', 'N'] end end From 99cb09c6f75bfc0889ccac2e9ec09f6958e45b44 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 20 May 2015 14:05:41 +1000 Subject: [PATCH 059/120] When loading products for shopfront, load all variants in one go --- app/controllers/shop_controller.rb | 28 ++++++++++++++++++--- app/serializers/api/product_serializer.rb | 14 +++-------- spec/controllers/shop_controller_spec.rb | 14 +++++++++++ spec/serializers/product_serializer_spec.rb | 14 ----------- 4 files changed, 42 insertions(+), 28 deletions(-) delete mode 100644 spec/serializers/product_serializer_spec.rb diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 73861def5c..35aece5e1d 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -10,12 +10,15 @@ class ShopController < BaseController end def products - # Can we make this query less slow? - # if @products = products_for_shop + render status: 200, - json: ActiveModel::ArraySerializer.new(@products, each_serializer: Api::ProductSerializer, - current_order_cycle: current_order_cycle, current_distributor: current_distributor).to_json + json: ActiveModel::ArraySerializer.new(@products, + each_serializer: Api::ProductSerializer, + current_order_cycle: current_order_cycle, + current_distributor: current_distributor, + variants: variants_for_shop_by_id).to_json + else render json: "", status: 404 end @@ -46,6 +49,23 @@ class ShopController < BaseController end end + def variants_for_shop_by_id + # We use the in_stock? method here instead of the in_stock scope because we need to + # look up the stock as overridden by VariantOverrides, and the scope method is not affected + # by them. + variants = Spree::Variant. + where(is_master: false). + for_distribution(current_order_cycle, current_distributor). + each { |v| v.scope_to_hub current_distributor }. + select(&:in_stock?) + + variants.inject({}) do |vs, v| + vs[v.product_id] ||= [] + vs[v.product_id] << v + vs + end + end + def taxon_order if current_distributor.preferred_shopfront_taxon_order.present? current_distributor diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index 0de794796b..b707aa281c 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -30,8 +30,9 @@ class Api::CachedProductSerializer < ActiveModel::Serializer #cached #delegate :cache_key, to: :object - attributes :id, :name, :permalink, :count_on_hand, :on_demand, :group_buy, - :notes, :description, :properties_with_values + attributes :id, :name, :permalink, :count_on_hand + attributes :on_demand, :group_buy, :notes, :description + attributes :properties_with_values has_many :variants, serializer: Api::VariantSerializer has_many :taxons, serializer: Api::IdSerializer @@ -46,13 +47,6 @@ class Api::CachedProductSerializer < ActiveModel::Serializer end def variants - # We use the in_stock? method here instead of the in_stock scope because we need to - # look up the stock as overridden by VariantOverrides, and the scope method is not affected - # by them. - - object.variants. - for_distribution(options[:current_order_cycle], options[:current_distributor]). - each { |v| v.scope_to_hub options[:current_distributor] }. - select(&:in_stock?) + options[:variants][object.id] || [] end end diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 4b7c53da19..10f0eed2cd 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -175,4 +175,18 @@ describe ShopController do end end end + + describe "loading variants" do + let(:hub) { create(:distributor_enterprise) } + let(:oc) { create(:simple_order_cycle, distributors: [hub], variants: [v1]) } + let(:p) { create(:simple_product) } + let!(:v1) { create(:variant, product: p, unit_value: 3) } + let!(:v2) { create(:variant, product: p, unit_value: 5) } + + it "scopes variants to distribution" do + controller.stub(:current_order_cycle) { oc } + controller.stub(:current_distributor) { hub } + controller.send(:variants_for_shop_by_id).should == {p.id => [v1]} + end + end end diff --git a/spec/serializers/product_serializer_spec.rb b/spec/serializers/product_serializer_spec.rb deleted file mode 100644 index 0091668dbb..0000000000 --- a/spec/serializers/product_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe Api::ProductSerializer do - let(:hub) { create(:distributor_enterprise) } - let(:oc) { create(:simple_order_cycle, distributors: [hub], variants: [v1]) } - let(:p) { create(:simple_product) } - let!(:v1) { create(:variant, product: p, unit_value: 3) } - let!(:v2) { create(:variant, product: p, unit_value: 5) } - - it "scopes variants to distribution" do - s = Api::ProductSerializer.new p, current_distributor: hub, current_order_cycle: oc - json = s.to_json - json.should include v1.options_text - json.should_not include v2.options_text - end -end From c5f00d87bd3b63a750f5282583084b84e6e31df6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 20 May 2015 15:22:01 +1000 Subject: [PATCH 060/120] When loading products for shopfront, load all master variants in one go --- app/controllers/shop_controller.rb | 46 ++++++++++++++--------- app/serializers/api/product_serializer.rb | 5 +++ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 35aece5e1d..0655624a0b 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -17,7 +17,8 @@ class ShopController < BaseController each_serializer: Api::ProductSerializer, current_order_cycle: current_order_cycle, current_distributor: current_distributor, - variants: variants_for_shop_by_id).to_json + variants: variants_for_shop_by_id, + master_variants: master_variants_for_shop_by_id).to_json else render json: "", status: 404 @@ -49,23 +50,6 @@ class ShopController < BaseController end end - def variants_for_shop_by_id - # We use the in_stock? method here instead of the in_stock scope because we need to - # look up the stock as overridden by VariantOverrides, and the scope method is not affected - # by them. - variants = Spree::Variant. - where(is_master: false). - for_distribution(current_order_cycle, current_distributor). - each { |v| v.scope_to_hub current_distributor }. - select(&:in_stock?) - - variants.inject({}) do |vs, v| - vs[v.product_id] ||= [] - vs[v.product_id] << v - vs - end - end - def taxon_order if current_distributor.preferred_shopfront_taxon_order.present? current_distributor @@ -76,4 +60,30 @@ class ShopController < BaseController "name ASC" end end + + def all_variants_for_shop + # We use the in_stock? method here instead of the in_stock scope because we need to + # look up the stock as overridden by VariantOverrides, and the scope method is not affected + # by them. + Spree::Variant. + for_distribution(current_order_cycle, current_distributor). + each { |v| v.scope_to_hub current_distributor }. + select(&:in_stock?) + end + + def variants_for_shop_by_id + index_by_product_id all_variants_for_shop.reject(&:is_master) + end + + def master_variants_for_shop_by_id + index_by_product_id all_variants_for_shop.select(&:is_master) + end + + def index_by_product_id(variants) + variants.inject({}) do |vs, v| + vs[v.product_id] ||= [] + vs[v.product_id] << v + vs + end + end end diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index b707aa281c..4c4aec2310 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -49,4 +49,9 @@ class Api::CachedProductSerializer < ActiveModel::Serializer def variants options[:variants][object.id] || [] end + + def master + options[:master_variants][object.id].andand.first + end + end From 769edbe9d52aebbb6cef71c3a06e90069f789b29 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 May 2015 12:04:59 +1000 Subject: [PATCH 061/120] Find the earliest closing times for each distributor in an active order cycle --- app/models/order_cycle.rb | 16 +++++++++++++++- spec/models/order_cycle_spec.rb | 31 +++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 88e4953edc..dfd45230b6 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -92,11 +92,25 @@ class OrderCycle < ActiveRecord::Base with_distributor(distributor).soonest_closing.first end - def self.most_recently_closed_for(distributor) with_distributor(distributor).most_recently_closed.first end + # Find the earliest closing times for each distributor in an active order cycle, and return + # them in the format {distributor_id => closing_time, ...} + def self.earliest_closing_times + Hash[ + Exchange. + outgoing. + joins(:order_cycle). + merge(OrderCycle.active). + group('exchanges.receiver_id'). + select('exchanges.receiver_id AS receiver_id, MIN(order_cycles.orders_close_at) AS earliest_close_at'). + map { |ex| [ex.receiver_id, ex.earliest_close_at.to_time] } + ] + end + + def clone! oc = self.dup oc.name = "COPY OF #{oc.name}" diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index 817d2e8e93..507f9977b1 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -313,7 +313,7 @@ describe OrderCycle do @oc.pickup_time_for(@d2).should == '2-8pm Friday' end end - + describe "finding pickup instructions for a distributor" do it "returns the pickup instructions" do @oc.pickup_instructions_for(@d1).should == "Come get it!" @@ -375,7 +375,7 @@ describe OrderCycle do occ.coordinator_fee_ids.should_not be_empty occ.coordinator_fee_ids.should == oc.coordinator_fee_ids - + # to_h gives us a unique hash for each exchange # check that the clone has no additional exchanges occ.exchanges.map(&:to_h).all? do |ex| @@ -402,7 +402,7 @@ describe OrderCycle do describe "finding order cycles opening in the future" do it "should give the soonest opening order cycle for a distributor" do distributor = create(:distributor_enterprise) - oc = create(:simple_order_cycle, name: 'oc 1', distributors: [distributor], orders_open_at: 10.days.from_now, orders_close_at: 11.days.from_now) + oc = create(:simple_order_cycle, name: 'oc 1', distributors: [distributor], orders_open_at: 10.days.from_now, orders_close_at: 11.days.from_now) OrderCycle.first_opening_for(distributor).should == oc end @@ -411,13 +411,32 @@ describe OrderCycle do OrderCycle.first_opening_for(distributor).should == nil end end - + describe "finding open order cycles" do it "should give the soonest closing order cycle for a distributor" do distributor = create(:distributor_enterprise) - oc = create(:simple_order_cycle, name: 'oc 1', distributors: [distributor], orders_open_at: 1.days.ago, orders_close_at: 11.days.from_now) - oc2 = create(:simple_order_cycle, name: 'oc 2', distributors: [distributor], orders_open_at: 2.days.ago, orders_close_at: 12.days.from_now) + oc = create(:simple_order_cycle, name: 'oc 1', distributors: [distributor], orders_open_at: 1.days.ago, orders_close_at: 11.days.from_now) + oc2 = create(:simple_order_cycle, name: 'oc 2', distributors: [distributor], orders_open_at: 2.days.ago, orders_close_at: 12.days.from_now) OrderCycle.first_closing_for(distributor).should == oc end end + + describe "finding the earliest closing times for each distributor" do + let(:time1) { 1.week.from_now } + let(:time2) { 2.weeks.from_now } + let(:time3) { 3.weeks.from_now } + let(:e1) { create(:distributor_enterprise) } + let(:e2) { create(:distributor_enterprise) } + let!(:oc1) { create(:simple_order_cycle, orders_close_at: time1, distributors: [e1]) } + let!(:oc2) { create(:simple_order_cycle, orders_close_at: time2, distributors: [e2]) } + let!(:oc3) { create(:simple_order_cycle, orders_close_at: time3, distributors: [e2]) } + + it "returns the closing time, indexed by enterprise id" do + OrderCycle.earliest_closing_times[e1.id].should == time1 + end + + it "returns the earliest closing time" do + OrderCycle.earliest_closing_times[e2.id].should == time2 + end + end end From f940984ca311313e3457da6ec98eb5c044c609f0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 May 2015 12:07:11 +1000 Subject: [PATCH 062/120] Pull earliest closing time computations out of the serialization loop --- app/helpers/injection_helper.rb | 2 +- app/serializers/api/enterprise_serializer.rb | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index 37794cef9d..a17d6e81f9 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -1,6 +1,6 @@ module InjectionHelper def inject_enterprises - inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, active_distributors: @active_distributors + inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, active_distributors: @active_distributors, earliest_closing_times: OrderCycle.earliest_closing_times end def inject_current_order diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 532887ae01..626fe77cad 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -18,14 +18,12 @@ class Api::UncachedEnterpriseSerializer < ActiveModel::Serializer attributes :orders_close_at, :active def orders_close_at - OrderCycle.first_closing_for(object).andand.orders_close_at + options[:earliest_closing_times][object.id] end def active - @options[:active_distributors].andand.include? object + options[:active_distributors].andand.include? object end - - end class Api::CachedEnterpriseSerializer < ActiveModel::Serializer From f0e909c92bc7a029ca9fb607accfaa9b4799c871 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 May 2015 12:38:33 +1000 Subject: [PATCH 063/120] Look up the shipping services (pickup, delivery) that different hubs provide --- app/models/spree/shipping_method_decorator.rb | 16 +++++++++++ spec/models/spree/shipping_method_spec.rb | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/app/models/spree/shipping_method_decorator.rb b/app/models/spree/shipping_method_decorator.rb index 4a2cf75b47..b8be603048 100644 --- a/app/models/spree/shipping_method_decorator.rb +++ b/app/models/spree/shipping_method_decorator.rb @@ -25,6 +25,22 @@ Spree::ShippingMethod.class_eval do scope :by_name, order('spree_shipping_methods.name ASC') + + # Return the services (pickup, delivery) that different distributors provide, in the format: + # {distributor_id => {pickup: true, delivery: false}, ...} + def self.services + Hash[ + Spree::ShippingMethod. + joins(:distributor_shipping_methods). + group('distributor_id'). + select("distributor_id"). + select("BOOL_OR(spree_shipping_methods.require_ship_address = 'f') AS pickup"). + select("BOOL_OR(spree_shipping_methods.require_ship_address = 't') AS delivery"). + map { |sm| [sm.distributor_id.to_i, {pickup: sm.pickup == 't', delivery: sm.delivery == 't'}] } + ] + end + + def available_to_order_with_distributor_check?(order, display_on=nil) available_to_order_without_distributor_check?(order, display_on) && self.distributors.include?(order.distributor) diff --git a/spec/models/spree/shipping_method_spec.rb b/spec/models/spree/shipping_method_spec.rb index d6b821e890..8b7f397191 100644 --- a/spec/models/spree/shipping_method_spec.rb +++ b/spec/models/spree/shipping_method_spec.rb @@ -55,5 +55,32 @@ module Spree sm.should be_available_to_order o end end + + describe "finding services offered by all distributors" do + let!(:d1) { create(:distributor_enterprise) } + let!(:d2) { create(:distributor_enterprise) } + let!(:d3) { create(:distributor_enterprise) } + let!(:d4) { create(:distributor_enterprise) } + let!(:d1_pickup) { create(:shipping_method, require_ship_address: false, distributors: [d1]) } + let!(:d1_delivery) { create(:shipping_method, require_ship_address: true, distributors: [d1]) } + let!(:d2_pickup) { create(:shipping_method, require_ship_address: false, distributors: [d2]) } + let!(:d3_delivery) { create(:shipping_method, require_ship_address: true, distributors: [d3]) } + + it "reports when the services are available" do + ShippingMethod.services[d1.id].should == {pickup: true, delivery: true} + end + + it "reports when only pickup is available" do + ShippingMethod.services[d2.id].should == {pickup: true, delivery: false} + end + + it "reports when only delivery is available" do + ShippingMethod.services[d3.id].should == {pickup: false, delivery: true} + end + + it "returns no entry when no service is available" do + ShippingMethod.services[d4.id].should be_nil + end + end end end From ee8db23fd99e08dc4037b8feef03079253276e77 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 May 2015 12:40:04 +1000 Subject: [PATCH 064/120] Pull shipping method service computations out of the serialization loop --- app/helpers/injection_helper.rb | 2 +- app/serializers/api/enterprise_serializer.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index a17d6e81f9..39831711ff 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -1,6 +1,6 @@ module InjectionHelper def inject_enterprises - inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, active_distributors: @active_distributors, earliest_closing_times: OrderCycle.earliest_closing_times + inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, active_distributors: @active_distributors, earliest_closing_times: OrderCycle.earliest_closing_times, shipping_method_services: Spree::ShippingMethod.services end def inject_current_order diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 626fe77cad..f7bd06f239 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -42,11 +42,13 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer has_one :address, serializer: Api::AddressSerializer def pickup - object.shipping_methods.where(:require_ship_address => false).present? + services = options[:shipping_method_services][object.id] + services ? services[:pickup] : false end def delivery - object.shipping_methods.where(:require_ship_address => true).present? + services = options[:shipping_method_services][object.id] + services ? services[:delivery] : false end def email From 704955a1854fbf2739779e5a5a6f6f2845535bba Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 May 2015 15:47:52 +1000 Subject: [PATCH 065/120] Load active distributors where they're needed rather than in most controllers --- app/controllers/base_controller.rb | 3 --- app/controllers/checkout_controller.rb | 3 --- app/controllers/enterprises_controller.rb | 2 +- app/controllers/groups_controller.rb | 1 - app/controllers/home_controller.rb | 4 +--- app/controllers/map_controller.rb | 1 - app/controllers/producers_controller.rb | 3 +-- app/helpers/injection_helper.rb | 18 +++++++++++++++++- app/views/groups/show.html.haml | 6 +++--- spec/controllers/base_controller_spec.rb | 5 ----- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index 1d74df5706..392e2fef64 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -12,9 +12,6 @@ class BaseController < ApplicationController before_filter :check_order_cycle_expiry - def load_active_distributors - @active_distributors ||= Enterprise.distributors_with_active_order_cycles - end private diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 0c42ceb987..11be28ef04 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -12,9 +12,6 @@ class CheckoutController < Spree::CheckoutController include EnterprisesHelper def edit - # Because this controller doesn't inherit from our BaseController - # We need to duplicate the code here - @active_distributors ||= Enterprise.distributors_with_active_order_cycles end def update diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 097139556c..75ad5c475b 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -4,7 +4,7 @@ class EnterprisesController < BaseController include OrderCyclesHelper # These prepended filters are in the reverse order of execution - prepend_before_filter :load_active_distributors, :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop + prepend_before_filter :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop before_filter :clean_permalink, only: :check_permalink respond_to :js, only: :permalink_checker diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 8653131b5a..6930632966 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,6 +1,5 @@ class GroupsController < BaseController layout 'darkswarm' - before_filter :load_active_distributors def index @groups = EnterpriseGroup.on_front_page.by_position diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 76e179ed22..3bb7a68538 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,11 +1,9 @@ class HomeController < BaseController layout 'darkswarm' - before_filter :load_active_distributors - + def index end def about_us end end - diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index a980ba8f40..46a6f5852a 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -1,6 +1,5 @@ class MapController < BaseController layout 'darkswarm' - before_filter :load_active_distributors def index end diff --git a/app/controllers/producers_controller.rb b/app/controllers/producers_controller.rb index b101a95b7f..42d1d401e5 100644 --- a/app/controllers/producers_controller.rb +++ b/app/controllers/producers_controller.rb @@ -1,7 +1,6 @@ class ProducersController < BaseController layout 'darkswarm' - before_filter :load_active_distributors - + def index end end diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index 39831711ff..2229888f98 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -1,6 +1,10 @@ module InjectionHelper def inject_enterprises - inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, active_distributors: @active_distributors, earliest_closing_times: OrderCycle.earliest_closing_times, shipping_method_services: Spree::ShippingMethod.services + inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, enterprise_injection_data + end + + def inject_group_enterprises + inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, enterprise_injection_data end def inject_current_order @@ -53,4 +57,16 @@ module InjectionHelper end render partial: "json/injection_ams", locals: {name: name, json: json} end + + + private + + def enterprise_injection_data + @active_distributors ||= Enterprise.distributors_with_active_order_cycles + @earliest_closing_times ||= OrderCycle.earliest_closing_times + @shipping_method_services ||= Spree::ShippingMethod.services + + {active_distributors: @active_distributors, earliest_closing_times: @earliest_closing_times, shipping_method_services: @shipping_method_services} + end + end diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 1bc965dfb3..80101d9e8c 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -3,8 +3,8 @@ = inject_enterprises -# inject enterprises in this group --# further hubs and producers of these enterprises can't be resoleved within this small subset -= inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, active_distributors: @active_distributors +-# further hubs and producers of these enterprises can't be resolved within this small subset += inject_group_enterprises #group-page.row.pad-top{"ng-controller" => "GroupPageCtrl"} .small-12.columns.pad-top @@ -95,7 +95,7 @@ = render partial: 'home/fat' = render partial: 'shared/components/enterprise_no_results' - + .small-12.medium-12.large-3.columns = render partial: 'contact' diff --git a/spec/controllers/base_controller_spec.rb b/spec/controllers/base_controller_spec.rb index 1040b0594c..b5ef006c5b 100644 --- a/spec/controllers/base_controller_spec.rb +++ b/spec/controllers/base_controller_spec.rb @@ -24,9 +24,4 @@ describe BaseController do response.should redirect_to root_url flash[:info].should == "The order cycle you've selected has just closed. Please try again!" end - - it "loads active_distributors" do - Enterprise.stub(:distributors_with_active_order_cycles) { 'active distributors' } - controller.load_active_distributors.should == 'active distributors' - end end From 4a59c85b3e83bcf02837c3665f911f90f2072411 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 May 2015 15:48:40 +1000 Subject: [PATCH 066/120] Inject current hub from AMS rather than RABL --- app/helpers/injection_helper.rb | 4 ++++ app/views/json/_current_hub.rabl | 6 ------ app/views/layouts/darkswarm.html.haml | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 app/views/json/_current_hub.rabl diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index 2229888f98..a0aa2f87f1 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -7,6 +7,10 @@ module InjectionHelper inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, enterprise_injection_data end + def inject_current_hub + inject_json_ams "currentHub", current_distributor, Api::EnterpriseSerializer, enterprise_injection_data + end + def inject_current_order inject_json_ams "currentOrder", current_order, Api::CurrentOrderSerializer, current_distributor: current_distributor, current_order_cycle: current_order_cycle end diff --git a/app/views/json/_current_hub.rabl b/app/views/json/_current_hub.rabl deleted file mode 100644 index 103baf9fb3..0000000000 --- a/app/views/json/_current_hub.rabl +++ /dev/null @@ -1,6 +0,0 @@ -object current_distributor -extends 'json/partials/enterprise' - -child suppliers: :producers do - attributes :id -end diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index a627ba4896..5169efa942 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -24,7 +24,7 @@ = render partial: "shared/ie_warning" = javascript_include_tag "iehack" - = inject_json "currentHub", "current_hub" + = inject_current_hub = inject_json "user", "current_user" = inject_json "railsFlash", "flash" = inject_taxons From cf79b90044c219757dc5e2044b9ea1e70944e3b1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 May 2015 16:24:13 +1000 Subject: [PATCH 067/120] Load relatives of all enterprises in one go --- app/models/enterprise_relationship.rb | 22 +++++++++++++++++++++ spec/models/enterprise_relationship_spec.rb | 12 +++++++++++ 2 files changed, 34 insertions(+) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index fbdef9d52c..22d04a6cd1 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -25,6 +25,28 @@ class EnterpriseRelationship < ActiveRecord::Base scope :by_name, with_enterprises.order('child_enterprises.name, parent_enterprises.name') + # Load an array of the relatives of each enterprise (ie. any enterprise related to it in + # either direction). This array is split into distributors and producers, and has the format: + # {enterprise_id => {distributors: [id, ...], producers: [id, ...]} } + def self.relatives + relationships = EnterpriseRelationship.includes(:child, :parent) + relatives = {} + + relationships.each do |r| + relatives[r.parent_id] ||= {distributors: [], producers: []} + relatives[r.child_id] ||= {distributors: [], producers: []} + + relatives[r.parent_id][:producers] << r.child_id if r.child.is_primary_producer + relatives[r.parent_id][:distributors] << r.child_id if r.child.is_distributor + + relatives[r.child_id][:producers] << r.parent_id if r.parent.is_primary_producer + relatives[r.child_id][:distributors] << r.parent_id if r.parent.is_distributor + end + + relatives + end + + def permissions_list=(perms) perms.andand.each { |name| permissions.build name: name } end diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index e87c204037..cb1743dec1 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -69,4 +69,16 @@ describe EnterpriseRelationship do EnterpriseRelationship.with_permission('two').sort.should == [er1, er2].sort end end + + describe "finding relatives" do + let(:e1) { create(:supplier_enterprise) } + let(:e2) { create(:supplier_enterprise, sells: 'any') } + let!(:er) { create(:enterprise_relationship, parent: e1, child: e2) } + + it "categorises enterprises into distributors and producers" do + EnterpriseRelationship.relatives.should == + {e1.id => {distributors: [e2.id], producers: [e2.id]}, + e2.id => {distributors: [], producers: [e1.id]}} + end + end end From 3afd636577ad2348687a098b01c0dec4d457126e Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 May 2015 16:25:05 +1000 Subject: [PATCH 068/120] Pull relatives computation out of the serialization loop --- app/helpers/injection_helper.rb | 3 ++- app/serializers/api/enterprise_serializer.rb | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index a0aa2f87f1..1d5f4d19e2 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -69,8 +69,9 @@ module InjectionHelper @active_distributors ||= Enterprise.distributors_with_active_order_cycles @earliest_closing_times ||= OrderCycle.earliest_closing_times @shipping_method_services ||= Spree::ShippingMethod.services + @relatives ||= EnterpriseRelationship.relatives - {active_distributors: @active_distributors, earliest_closing_times: @earliest_closing_times, shipping_method_services: @shipping_method_services} + {active_distributors: @active_distributors, earliest_closing_times: @earliest_closing_times, shipping_method_services: @shipping_method_services, relatives: @relatives} end end diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index f7bd06f239..a97544b3ee 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -72,11 +72,13 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer end def producers - ActiveModel::ArraySerializer.new(object.suppliers.activated, {each_serializer: Api::IdSerializer}) + relatives = options[:relatives][object.id] + relatives ? relatives[:producers] : [] end def hubs - ActiveModel::ArraySerializer.new(object.distributors.activated, {each_serializer: Api::IdSerializer}) + relatives = options[:relatives][object.id] + relatives ? relatives[:distributors] : [] end # Map svg icons. From 2c92b5a7516b888d87583ad6e35a876bc583d504 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 22 May 2015 10:57:14 +1000 Subject: [PATCH 069/120] Find all supplied and distributed taxons --- app/models/spree/taxon_decorator.rb | 36 +++++++++++++++++++++++++++++ spec/models/spree/taxon_spec.rb | 28 ++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 spec/models/spree/taxon_spec.rb diff --git a/app/models/spree/taxon_decorator.rb b/app/models/spree/taxon_decorator.rb index 10ee0b4719..1a26ce73a8 100644 --- a/app/models/spree/taxon_decorator.rb +++ b/app/models/spree/taxon_decorator.rb @@ -9,4 +9,40 @@ Spree::Taxon.class_eval do #fs << Spree::ProductFilters.distributor_filter if Spree::ProductFilters.respond_to? :distributor_filter fs end + + # Find all the taxons of supplied products for each enterprise, indexed by enterprise. + # Format: {enterprise_id => [taxon_id, ...]} + def self.supplied_taxons + taxons = {} + + Spree::Taxon. + joins(:products => :supplier). + select('spree_taxons.*, enterprises.id AS enterprise_id'). + each do |t| + + taxons[t.enterprise_id.to_i] ||= Set.new + taxons[t.enterprise_id.to_i] << t.id + end + + taxons + end + + # Find all the taxons of distributed products for each enterprise, indexed by enterprise. + # Format: {enterprise_id => [taxon_id, ...]} + def self.distributed_taxons + taxons = {} + + Spree::Taxon. + joins(:products). + merge(Spree::Product.with_order_cycles_outer). + where('o_exchanges.incoming = ?', false). + select('spree_taxons.*, o_exchanges.receiver_id AS enterprise_id'). + each do |t| + + taxons[t.enterprise_id.to_i] ||= Set.new + taxons[t.enterprise_id.to_i] << t.id + end + + taxons + end end diff --git a/spec/models/spree/taxon_spec.rb b/spec/models/spree/taxon_spec.rb new file mode 100644 index 0000000000..a0d729c054 --- /dev/null +++ b/spec/models/spree/taxon_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +module Spree + describe Taxon do + let(:e) { create(:supplier_enterprise) } + let(:t0) { p1.taxons.order('id ASC').first } + let(:t1) { create(:taxon) } + let(:t2) { create(:taxon) } + + describe "finding all supplied taxons" do + let!(:p1) { create(:simple_product, supplier: e, taxons: [t1, t2]) } + + it "finds taxons" do + Taxon.supplied_taxons.should == {e.id => Set.new([t0.id, t1.id, t2.id])} + end + end + + describe "finding all distributed taxons" do + let!(:oc) { create(:simple_order_cycle, distributors: [e], variants: [p1.master]) } + let(:s) { create(:supplier_enterprise) } + let(:p1) { create(:simple_product, supplier: s, taxons: [t1, t2]) } + + it "finds taxons" do + Taxon.distributed_taxons.should == {e.id => Set.new([t0.id, t1.id, t2.id])} + end + end + end +end From 1a887df4127d932f2f0f40d54a4ffc9f416aa65b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 22 May 2015 11:03:53 +1000 Subject: [PATCH 070/120] Pull taxon computation out of the serialization loop --- app/helpers/injection_helper.rb | 4 +++- app/serializers/api/enterprise_serializer.rb | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index 1d5f4d19e2..c2e4e6c91a 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -70,8 +70,10 @@ module InjectionHelper @earliest_closing_times ||= OrderCycle.earliest_closing_times @shipping_method_services ||= Spree::ShippingMethod.services @relatives ||= EnterpriseRelationship.relatives + @supplied_taxons ||= Spree::Taxon.supplied_taxons + @distributed_taxons ||= Spree::Taxon.distributed_taxons - {active_distributors: @active_distributors, earliest_closing_times: @earliest_closing_times, shipping_method_services: @shipping_method_services, relatives: @relatives} + {active_distributors: @active_distributors, earliest_closing_times: @earliest_closing_times, shipping_method_services: @shipping_method_services, relatives: @relatives, supplied_taxons: @supplied_taxons, distributed_taxons: @distributed_taxons} end end diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index a97544b3ee..d15ee7860a 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -36,11 +36,18 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer :email, :hash, :logo, :promo_image, :path, :pickup, :delivery, :icon, :icon_font, :producer_icon_font, :category, :producers, :hubs - has_many :distributed_taxons, key: :taxons, serializer: Api::IdSerializer - has_many :supplied_taxons, serializer: Api::IdSerializer + attributes :taxons, :supplied_taxons has_one :address, serializer: Api::AddressSerializer + def taxons + options[:distributed_taxons][object.id] + end + + def supplied_taxons + options[:supplied_taxons][object.id] + end + def pickup services = options[:shipping_method_services][object.id] services ? services[:pickup] : false From dd761719eea337c807645ae32a3f7909a81f2e54 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 22 May 2015 11:27:47 +1000 Subject: [PATCH 071/120] Fix undefined Api::IdSerializer error --- app/serializers/api/enterprise_serializer.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index d15ee7860a..6f46d62eda 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -1,4 +1,7 @@ class Api::EnterpriseSerializer < ActiveModel::Serializer + # We reference this here because otherwise the serializer complains about its absence + Api::IdSerializer + def serializable_hash cached_serializer_hash.merge uncached_serializer_hash end @@ -40,6 +43,7 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer has_one :address, serializer: Api::AddressSerializer + def taxons options[:distributed_taxons][object.id] end From 31b726613d5116dc2a1f615aa0f16f55d6f58559 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 22 May 2015 12:17:09 +1000 Subject: [PATCH 072/120] Avoid loading enterprise injection data when it's not be needed due to caching --- app/helpers/injection_helper.rb | 14 ++++------ app/serializers/api/enterprise_serializer.rb | 16 +++++------ .../enterprise_injection_data.rb | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 lib/open_food_network/enterprise_injection_data.rb diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index c2e4e6c91a..05057c136b 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -1,6 +1,8 @@ +require 'open_food_network/enterprise_injection_data' + module InjectionHelper def inject_enterprises - inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, enterprise_injection_data + inject_json_ams "enterprises", Enterprise.activated.includes(:address).all, Api::EnterpriseSerializer, enterprise_injection_data end def inject_group_enterprises @@ -66,14 +68,8 @@ module InjectionHelper private def enterprise_injection_data - @active_distributors ||= Enterprise.distributors_with_active_order_cycles - @earliest_closing_times ||= OrderCycle.earliest_closing_times - @shipping_method_services ||= Spree::ShippingMethod.services - @relatives ||= EnterpriseRelationship.relatives - @supplied_taxons ||= Spree::Taxon.supplied_taxons - @distributed_taxons ||= Spree::Taxon.distributed_taxons - - {active_distributors: @active_distributors, earliest_closing_times: @earliest_closing_times, shipping_method_services: @shipping_method_services, relatives: @relatives, supplied_taxons: @supplied_taxons, distributed_taxons: @distributed_taxons} + @enterprise_injection_data ||= OpenFoodNetwork::EnterpriseInjectionData.new + {data: @enterprise_injection_data} end end diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 6f46d62eda..4f38c12964 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -21,11 +21,11 @@ class Api::UncachedEnterpriseSerializer < ActiveModel::Serializer attributes :orders_close_at, :active def orders_close_at - options[:earliest_closing_times][object.id] + options[:data].earliest_closing_times[object.id] end def active - options[:active_distributors].andand.include? object + options[:data].active_distributors.andand.include? object end end @@ -45,20 +45,20 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer def taxons - options[:distributed_taxons][object.id] + options[:data].distributed_taxons[object.id] end def supplied_taxons - options[:supplied_taxons][object.id] + options[:data].supplied_taxons[object.id] end def pickup - services = options[:shipping_method_services][object.id] + services = options[:data].shipping_method_services[object.id] services ? services[:pickup] : false end def delivery - services = options[:shipping_method_services][object.id] + services = options[:data].shipping_method_services[object.id] services ? services[:delivery] : false end @@ -83,12 +83,12 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer end def producers - relatives = options[:relatives][object.id] + relatives = options[:data].relatives[object.id] relatives ? relatives[:producers] : [] end def hubs - relatives = options[:relatives][object.id] + relatives = options[:data].relatives[object.id] relatives ? relatives[:distributors] : [] end diff --git a/lib/open_food_network/enterprise_injection_data.rb b/lib/open_food_network/enterprise_injection_data.rb new file mode 100644 index 0000000000..9862418b98 --- /dev/null +++ b/lib/open_food_network/enterprise_injection_data.rb @@ -0,0 +1,27 @@ +module OpenFoodNetwork + class EnterpriseInjectionData + def active_distributors + @active_distributors ||= Enterprise.distributors_with_active_order_cycles + end + + def earliest_closing_times + @earliest_closing_times ||= OrderCycle.earliest_closing_times + end + + def shipping_method_services + @shipping_method_services ||= Spree::ShippingMethod.services + end + + def relatives + @relatives ||= EnterpriseRelationship.relatives + end + + def supplied_taxons + @supplied_taxons ||= Spree::Taxon.supplied_taxons + end + + def distributed_taxons + @distributed_taxons ||= Spree::Taxon.distributed_taxons + end + end +end From e1b4c3b1e4bb75efe713c8b246fdb835004caa36 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 22 May 2015 13:47:24 +1000 Subject: [PATCH 073/120] Add benchmarking test for inject_enterprises --- spec/performance/injection_helper_spec.rb | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 spec/performance/injection_helper_spec.rb diff --git a/spec/performance/injection_helper_spec.rb b/spec/performance/injection_helper_spec.rb new file mode 100644 index 0000000000..4ba22d738f --- /dev/null +++ b/spec/performance/injection_helper_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe InjectionHelper, type: :helper do + let(:oc) { create(:simple_order_cycle) } + let(:relative_supplier) { create(:supplier_enterprise) } + let(:relative_distributor) { create(:distributor_enterprise) } + + before do + 50.times do + e = create(:enterprise) + oc.distributors << e + create(:enterprise_relationship, parent: e, child: relative_supplier) + create(:enterprise_relationship, parent: e, child: relative_distributor) + end + end + + it "is performant in injecting enterprises" do + results = [] + 4.times do |i| + ActiveRecord::Base.connection.query_cache.clear + Rails.cache.clear + result = Benchmark.measure { helper.inject_enterprises } + results << result.total if i > 0 + puts result + end + + puts (results.sum / results.count * 1000).round 0 + end +end From 41bc67e2d8e601b111c8e3820206b79e8cf1e04f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 22 May 2015 14:46:33 +1000 Subject: [PATCH 074/120] Add benchmark for product serialisation --- spec/performance/shop_controller_spec.rb | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 spec/performance/shop_controller_spec.rb diff --git a/spec/performance/shop_controller_spec.rb b/spec/performance/shop_controller_spec.rb new file mode 100644 index 0000000000..14e258d18d --- /dev/null +++ b/spec/performance/shop_controller_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe ShopController, type: :controller do + let(:d) { create(:distributor_enterprise) } + let(:enterprise_fee) { create(:enterprise_fee) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator_fees: [enterprise_fee]) } + + before do + controller.stub(:current_distributor) { d } + controller.stub(:current_order_cycle) { order_cycle } + end + + describe "fetching products" do + let(:exchange) { order_cycle.exchanges.to_enterprises(d).outgoing.first } + let(:image) { File.open(File.expand_path('../../../app/assets/images/logo.jpg', __FILE__)) } + + before do + 11.times do + p = create(:simple_product) + p.set_property 'Organic Certified', 'NASAA 12345' + v1 = create(:variant, product: p) + v2 = create(:variant, product: p) + Spree::Image.create! viewable_id: p.master.id, viewable_type: 'Spree::Variant', attachment: image + + exchange.variants << [v1, v2] + end + end + + it "returns products via json" do + results = [] + 4.times do |i| + ActiveRecord::Base.connection.query_cache.clear + Rails.cache.clear + result = Benchmark.measure do + xhr :get, :products + response.should be_success + end + + results << result.total if i > 0 + puts result + end + + puts (results.sum / results.count * 1000).round 0 + end + end +end From e74390a013b9b614375c98bd63eb847b804923ed Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 27 May 2015 16:26:08 +1000 Subject: [PATCH 075/120] Remove controller specs for @active_distributors, now set via helper --- spec/controllers/home_controller_spec.rb | 12 ------------ spec/controllers/map_controller_spec.rb | 13 ------------- spec/controllers/producers_controller_spec.rb | 15 --------------- 3 files changed, 40 deletions(-) delete mode 100644 spec/controllers/map_controller_spec.rb delete mode 100644 spec/controllers/producers_controller_spec.rb diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index bbbff9a7b1..924462741f 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -9,21 +9,9 @@ describe HomeController do Enterprise.stub(:distributors_with_active_order_cycles) { [distributor] } end - it "sets active distributors" do - get :index - assigns[:active_distributors].should == [distributor] - end - # Exclusion from actual rendered view handled in features/consumer/home it "shows JSON for invisible hubs" do get :index response.body.should have_content invisible_distributor.name end - - # This is done inside the json/hubs Serializer - it "gets the next order cycle for each hub" do - OrderCycle.should_receive(:first_closing_for).twice - get :index - end end - diff --git a/spec/controllers/map_controller_spec.rb b/spec/controllers/map_controller_spec.rb deleted file mode 100644 index fab9b7ac22..0000000000 --- a/spec/controllers/map_controller_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' - -describe MapController do - it "loads active distributors" do - active_distributors = double(:distributors) - - Enterprise.stub(:distributors_with_active_order_cycles) { active_distributors } - - get :index - - assigns(:active_distributors).should == active_distributors - end -end diff --git a/spec/controllers/producers_controller_spec.rb b/spec/controllers/producers_controller_spec.rb deleted file mode 100644 index ec3c39036c..0000000000 --- a/spec/controllers/producers_controller_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe ProducersController do - let!(:distributor) { create(:distributor_enterprise) } - - before do - Enterprise.stub(:distributors_with_active_order_cycles) { [distributor] } - Enterprise.stub(:all).and_return([distributor]) - end - - it "sets active distributors" do - get :index - assigns[:active_distributors].should == [distributor] - end -end From 75f1f673ad3d63a6c366947876ce3479b62f050a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 27 May 2015 16:26:31 +1000 Subject: [PATCH 076/120] Update spec for EnterpriseSerializer --- spec/serializers/enterprise_serializer_spec.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/spec/serializers/enterprise_serializer_spec.rb b/spec/serializers/enterprise_serializer_spec.rb index 1063a042e7..7c467d9c3d 100644 --- a/spec/serializers/enterprise_serializer_spec.rb +++ b/spec/serializers/enterprise_serializer_spec.rb @@ -3,19 +3,18 @@ describe Api::EnterpriseSerializer do let(:enterprise) { create(:distributor_enterprise) } let(:taxon) { create(:taxon) } + let(:data_class) { Struct.new(:earliest_closing_times, :active_distributors, + :distributed_taxons, :supplied_taxons, + :shipping_method_services, :relatives) } + let(:data) { data_class.new({}, [], {}, {}, {}, {producers: [], distributors: []}) } + it "serializes an enterprise" do - serializer = Api::EnterpriseSerializer.new enterprise + serializer = Api::EnterpriseSerializer.new enterprise, data: data serializer.to_json.should match enterprise.name end - it "includes distributed taxons" do - enterprise.stub(:distributed_taxons).and_return [taxon] - serializer = Api::EnterpriseSerializer.new enterprise - serializer.to_json.should match taxon.id.to_s - end - it "will render urls" do - serializer = Api::EnterpriseSerializer.new enterprise + serializer = Api::EnterpriseSerializer.new enterprise, data: data serializer.to_json.should match "map_005-hub.svg" end end From 3ab7df88e68bf024039f1a59ca64283b3daaef95 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 27 May 2015 16:26:40 +1000 Subject: [PATCH 077/120] Allow serialization of nil enterprise --- app/serializers/api/enterprise_serializer.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 4f38c12964..fc843a5138 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -3,17 +3,18 @@ class Api::EnterpriseSerializer < ActiveModel::Serializer Api::IdSerializer def serializable_hash + cached_serializer_hash.merge uncached_serializer_hash end private def cached_serializer_hash - Api::CachedEnterpriseSerializer.new(object, @options).serializable_hash + Api::CachedEnterpriseSerializer.new(object, @options).serializable_hash || {} end def uncached_serializer_hash - Api::UncachedEnterpriseSerializer.new(object, @options).serializable_hash + Api::UncachedEnterpriseSerializer.new(object, @options).serializable_hash || {} end end @@ -31,7 +32,12 @@ end class Api::CachedEnterpriseSerializer < ActiveModel::Serializer cached - delegate :cache_key, to: :object + #delegate :cache_key, to: :object + + def cache_key + object.andand.cache_key + end + attributes :name, :id, :description, :latitude, :longitude, :long_description, :website, :instagram, :linkedin, :twitter, From cdbf02ca20f57b5d776bbe65422c620097e39476 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 29 May 2015 12:07:43 +1000 Subject: [PATCH 078/120] EnterpriseRelationship.relatives can find activated enterprises only --- app/models/enterprise_relationship.rb | 14 +++++++++----- spec/models/enterprise_relationship_spec.rb | 10 ++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 22d04a6cd1..49c26cb0f8 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -28,7 +28,7 @@ class EnterpriseRelationship < ActiveRecord::Base # Load an array of the relatives of each enterprise (ie. any enterprise related to it in # either direction). This array is split into distributors and producers, and has the format: # {enterprise_id => {distributors: [id, ...], producers: [id, ...]} } - def self.relatives + def self.relatives(activated_only=false) relationships = EnterpriseRelationship.includes(:child, :parent) relatives = {} @@ -36,11 +36,15 @@ class EnterpriseRelationship < ActiveRecord::Base relatives[r.parent_id] ||= {distributors: [], producers: []} relatives[r.child_id] ||= {distributors: [], producers: []} - relatives[r.parent_id][:producers] << r.child_id if r.child.is_primary_producer - relatives[r.parent_id][:distributors] << r.child_id if r.child.is_distributor + if !activated_only || r.child.activated? + relatives[r.parent_id][:producers] << r.child_id if r.child.is_primary_producer + relatives[r.parent_id][:distributors] << r.child_id if r.child.is_distributor + end - relatives[r.child_id][:producers] << r.parent_id if r.parent.is_primary_producer - relatives[r.child_id][:distributors] << r.parent_id if r.parent.is_distributor + if !activated_only || r.parent.activated? + relatives[r.child_id][:producers] << r.parent_id if r.parent.is_primary_producer + relatives[r.child_id][:distributors] << r.parent_id if r.parent.is_distributor + end end relatives diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index cb1743dec1..59e36c37e5 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -80,5 +80,15 @@ describe EnterpriseRelationship do {e1.id => {distributors: [e2.id], producers: [e2.id]}, e2.id => {distributors: [], producers: [e1.id]}} end + + it "finds inactive enterprises by default" do + e1.update_attribute :confirmed_at, nil + EnterpriseRelationship.relatives[e2.id][:producers].should == [e1.id] + end + + it "does not find inactive enterprises when requested" do + e1.update_attribute :confirmed_at, nil + EnterpriseRelationship.relatives(true)[e2.id][:producers].should be_empty + end end end From 69c54e1d704ebd15f5b935e0c93be619b24dc456 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 29 May 2015 12:08:21 +1000 Subject: [PATCH 079/120] Only load activated relatives for EnterpriseInjectionData --- app/models/enterprise.rb | 4 ++++ .../enterprise_injection_data.rb | 2 +- .../enterprise_injection_data_spec.rb | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 spec/lib/open_food_network/enterprise_injection_data_spec.rb diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index e921167cf0..61d441fb2e 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -178,6 +178,10 @@ class Enterprise < ActiveRecord::Base count(distinct: true) end + def activated? + confirmed_at.present? && sells != 'unspecified' + end + def set_producer_property(property_name, property_value) transaction do property = Spree::Property.where(name: property_name).first_or_create!(presentation: property_name) diff --git a/lib/open_food_network/enterprise_injection_data.rb b/lib/open_food_network/enterprise_injection_data.rb index 9862418b98..87516007c6 100644 --- a/lib/open_food_network/enterprise_injection_data.rb +++ b/lib/open_food_network/enterprise_injection_data.rb @@ -13,7 +13,7 @@ module OpenFoodNetwork end def relatives - @relatives ||= EnterpriseRelationship.relatives + @relatives ||= EnterpriseRelationship.relatives(true) end def supplied_taxons diff --git a/spec/lib/open_food_network/enterprise_injection_data_spec.rb b/spec/lib/open_food_network/enterprise_injection_data_spec.rb new file mode 100644 index 0000000000..cb94f2374a --- /dev/null +++ b/spec/lib/open_food_network/enterprise_injection_data_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +module OpenFoodNetwork + describe EnterpriseInjectionData do + describe "relatives" do + let!(:enterprise) { create(:distributor_enterprise) } + let!(:producer) { create(:supplier_enterprise) } + let!(:producer_inactive) { create(:supplier_enterprise, confirmed_at: nil) } + let!(:er_p) { create(:enterprise_relationship, parent: producer, child: enterprise) } + let!(:er_pi) { create(:enterprise_relationship, parent: producer_inactive, child: enterprise) } + + it "only loads activated relatives" do + subject.relatives[enterprise.id][:producers].should_not include producer_inactive.id + end + end + end +end From 3f4f8afacd8b8ca432861da0fa8f6b92e68953df Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 29 May 2015 12:19:38 +1000 Subject: [PATCH 080/120] EnterpriseRelationship.relatives does not show duplicates --- app/models/enterprise_relationship.rb | 4 ++-- spec/models/enterprise_relationship_spec.rb | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 49c26cb0f8..de06a0578e 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -33,8 +33,8 @@ class EnterpriseRelationship < ActiveRecord::Base relatives = {} relationships.each do |r| - relatives[r.parent_id] ||= {distributors: [], producers: []} - relatives[r.child_id] ||= {distributors: [], producers: []} + relatives[r.parent_id] ||= {distributors: Set.new, producers: Set.new} + relatives[r.child_id] ||= {distributors: Set.new, producers: Set.new} if !activated_only || r.child.activated? relatives[r.parent_id][:producers] << r.child_id if r.child.is_primary_producer diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index 59e36c37e5..96f80a65a4 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -74,21 +74,27 @@ describe EnterpriseRelationship do let(:e1) { create(:supplier_enterprise) } let(:e2) { create(:supplier_enterprise, sells: 'any') } let!(:er) { create(:enterprise_relationship, parent: e1, child: e2) } + let(:er_reverse) { create(:enterprise_relationship, parent: e2, child: e1) } it "categorises enterprises into distributors and producers" do EnterpriseRelationship.relatives.should == - {e1.id => {distributors: [e2.id], producers: [e2.id]}, - e2.id => {distributors: [], producers: [e1.id]}} + {e1.id => {distributors: Set.new([e2.id]), producers: Set.new([e2.id])}, + e2.id => {distributors: Set.new([]), producers: Set.new([e1.id])}} end it "finds inactive enterprises by default" do e1.update_attribute :confirmed_at, nil - EnterpriseRelationship.relatives[e2.id][:producers].should == [e1.id] + EnterpriseRelationship.relatives[e2.id][:producers].should == Set.new([e1.id]) end it "does not find inactive enterprises when requested" do e1.update_attribute :confirmed_at, nil EnterpriseRelationship.relatives(true)[e2.id][:producers].should be_empty end + + it "does not show duplicates" do + er_reverse + EnterpriseRelationship.relatives[e2.id][:producers].should == Set.new([e1.id]) + end end end From d478cc1f69a1ed7d4436ef5e323acaec5de5d39c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 29 May 2015 14:03:44 +1000 Subject: [PATCH 081/120] Serialize taxons and relatives in expected format --- app/serializers/api/enterprise_serializer.rb | 16 ++++++++---- .../serializers/enterprise_serializer_spec.rb | 25 +++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index fc843a5138..44364c4dbc 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -3,7 +3,6 @@ class Api::EnterpriseSerializer < ActiveModel::Serializer Api::IdSerializer def serializable_hash - cached_serializer_hash.merge uncached_serializer_hash end @@ -51,11 +50,11 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer def taxons - options[:data].distributed_taxons[object.id] + ids_to_objs options[:data].distributed_taxons[object.id] end def supplied_taxons - options[:data].supplied_taxons[object.id] + ids_to_objs options[:data].supplied_taxons[object.id] end def pickup @@ -90,12 +89,12 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer def producers relatives = options[:data].relatives[object.id] - relatives ? relatives[:producers] : [] + relatives ? ids_to_objs(relatives[:producers]) : [] end def hubs relatives = options[:data].relatives[object.id] - relatives ? relatives[:distributors] : [] + relatives ? ids_to_objs(relatives[:distributors]) : [] end # Map svg icons. @@ -135,4 +134,11 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer } icon_fonts[object.category] end + + + private + + def ids_to_objs(ids) + ids.andand.map { |id| {id: id} } + end end diff --git a/spec/serializers/enterprise_serializer_spec.rb b/spec/serializers/enterprise_serializer_spec.rb index 7c467d9c3d..f70852d973 100644 --- a/spec/serializers/enterprise_serializer_spec.rb +++ b/spec/serializers/enterprise_serializer_spec.rb @@ -1,20 +1,31 @@ #require 'spec_helper' describe Api::EnterpriseSerializer do + let(:serializer) { Api::EnterpriseSerializer.new enterprise, data: data } let(:enterprise) { create(:distributor_enterprise) } let(:taxon) { create(:taxon) } - let(:data_class) { Struct.new(:earliest_closing_times, :active_distributors, - :distributed_taxons, :supplied_taxons, - :shipping_method_services, :relatives) } - let(:data) { data_class.new({}, [], {}, {}, {}, {producers: [], distributors: []}) } + let(:data) { OpenStruct.new(earliest_closing_times: {}, + active_distributors: [], + distributed_taxons: {enterprise.id => [123]}, + supplied_taxons: {enterprise.id => [456]}, + shipping_method_services: {}, + relatives: {enterprise.id => {producers: [123], distributors: [456]}}) } it "serializes an enterprise" do - serializer = Api::EnterpriseSerializer.new enterprise, data: data serializer.to_json.should match enterprise.name end - it "will render urls" do - serializer = Api::EnterpriseSerializer.new enterprise, data: data + it "serializes taxons as ids only" do + serializer.serializable_hash[:taxons].should == [{id: 123}] + serializer.serializable_hash[:supplied_taxons].should == [{id: 456}] + end + + it "serializes producers and hubs as ids only" do + serializer.serializable_hash[:producers].should == [{id: 123}] + serializer.serializable_hash[:hubs].should == [{id: 456}] + end + + it "serializes icons" do serializer.to_json.should match "map_005-hub.svg" end end From 503b687ed1f7ea10a546aac92682671989127a54 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 29 May 2015 15:19:05 +1000 Subject: [PATCH 082/120] Display distributor banner only if current_distributor is present --- app/views/spree/orders/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/orders/show.html.haml b/app/views/spree/orders/show.html.haml index 82120add82..de142e3cac 100644 --- a/app/views/spree/orders/show.html.haml +++ b/app/views/spree/orders/show.html.haml @@ -9,7 +9,7 @@ - else = @order.distributor.next_collection_at - = render "shopping_shared/details" + = render "shopping_shared/details" if current_distributor.present? %fieldset#order_summary{"data-hook" => ""} .row From 678b591c1845f18673476f3d605b6ffda5b84c4a Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 29 May 2015 12:43:11 +1000 Subject: [PATCH 083/120] Explain how to disable delayed jobs to send emails again --- config/initializers/delayed_job.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/initializers/delayed_job.rb b/config/initializers/delayed_job.rb index 8fc9aa8ec7..80dc11d3aa 100644 --- a/config/initializers/delayed_job.rb +++ b/config/initializers/delayed_job.rb @@ -2,6 +2,10 @@ Delayed::Worker.logger = Logger.new(Rails.root.join('log', 'delayed_job.log')) Delayed::Worker.destroy_failed_jobs = false Delayed::Worker.max_run_time = 15.minutes +# Uncomment the next line if you want jobs to be executed straight away. +# For example you want emails to be opened in your browser while testing. +#Delayed::Worker.delay_jobs = false + # Notify bugsnag when a job fails # Code adapted from http://trevorturk.com/2011/01/25/notify-hoptoad-if-theres-an-exception-in-delayedjob/ class Delayed::Worker From bf935623dcfbd06c462b9aad5b5093b7d428106f Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 29 May 2015 13:44:53 +1000 Subject: [PATCH 084/120] changing default mailer url in development from test.com to 0.0.0.0:3000 --- config/environments/development.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index efa229e33c..200484122a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -31,7 +31,7 @@ Openfoodnetwork::Application.configure do # Show emails using Letter Opener config.action_mailer.delivery_method = :letter_opener - config.action_mailer.default_url_options = { host: "test.com" } + config.action_mailer.default_url_options = { host: "0.0.0.0:3000" } end From fe27b1d446358231af4c85e5a1ecb9212878a9ea Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 29 May 2015 13:46:16 +1000 Subject: [PATCH 085/120] text changes to the registration wizard --- .../templates/registration/about.html.haml | 2 +- .../templates/registration/finished.html.haml | 2 +- .../registration/introduction.html.haml | 3 +- .../templates/registration/type.html.haml | 8 ++- .../confirmation_instructions.html.haml | 12 ++-- app/views/enterprise_mailer/welcome.html.haml | 71 +++++-------------- .../user_mailer/signup_confirmation.html.haml | 4 +- 7 files changed, 38 insertions(+), 64 deletions(-) diff --git a/app/assets/javascripts/templates/registration/about.html.haml b/app/assets/javascripts/templates/registration/about.html.haml index be9948b95d..9c57240dfe 100644 --- a/app/assets/javascripts/templates/registration/about.html.haml +++ b/app/assets/javascripts/templates/registration/about.html.haml @@ -14,7 +14,7 @@ .small-12.columns .alert-box.info{ "ofn-inline-alert" => true, ng: { show: "visible" } } %h6 Success! {{ enterprise.name }} added to the Open Food Network - %span If you exit the wizard at any stage, login and go to admin to edit or update your enterprise details. + %span If you exit this wizard at any stage, you need to click the confirmation link in the email you have received. This will take you to your admin interface where you can continue setting up your profile. %a.close{ ng: { click: "close()" } } × .small-12.large-8.columns diff --git a/app/assets/javascripts/templates/registration/finished.html.haml b/app/assets/javascripts/templates/registration/finished.html.haml index f647a2d8bb..46cca9daf5 100644 --- a/app/assets/javascripts/templates/registration/finished.html.haml +++ b/app/assets/javascripts/templates/registration/finished.html.haml @@ -18,7 +18,7 @@ %p We've sent a confirmation email to - %strong {{ enterprise.email }}. + %strong {{ enterprise.email }} if it hasn't been activated before. %br Please follow the instructions there to make your enterprise visible on the Open Food Network. %a.button.primary{ type: "button", href: "/" } Open Food Network home > diff --git a/app/assets/javascripts/templates/registration/introduction.html.haml b/app/assets/javascripts/templates/registration/introduction.html.haml index 60a8547b4a..48553de09a 100644 --- a/app/assets/javascripts/templates/registration/introduction.html.haml +++ b/app/assets/javascripts/templates/registration/introduction.html.haml @@ -5,7 +5,7 @@ %h4 %small %i.ofn-i_040-hub - Create your enterprise profile + You can now create a profile for your Producer or Hub .hide-for-large-up %hr %input.button.small.primary{ type: "button", value: "Let's get started!", ng: { click: "select('details')" } } @@ -38,6 +38,7 @@ %strong contact you on the Open Food Network. %p Use this space to tell the story of your enterprise, to help drive connections to your social and online presence. + %p It's also the first step towards trading on the Open Food Network, or opening an online store. .row.show-for-large-up .small-12.columns diff --git a/app/assets/javascripts/templates/registration/type.html.haml b/app/assets/javascripts/templates/registration/type.html.haml index 48d45cb66a..9593bfa89c 100644 --- a/app/assets/javascripts/templates/registration/type.html.haml +++ b/app/assets/javascripts/templates/registration/type.html.haml @@ -38,9 +38,13 @@ %i.ofn-i_013-help   %p Producers make yummy things to eat &/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it. - / %p Hubs connect the producer to the eater. Hubs can be co-ops, independent retailers, buying groups, wholesalers, CSA box schemes, farm-gate stalls, etc. + .panel.callout + .left + %i.ofn-i_013-help +   + %p If you’re not a producer, you’re probably someone who sells and distributes food. You might be a hub, coop, buying group, retailer, wholesaler or other. .row.buttons .small-12.columns %input.button.secondary{ type: "button", value: "Back", ng: { click: "select('contact')" } } - %input.button.primary.right{ type: "submit", value: "Continue" } + %input.button.primary.right{ type: "submit", value: "Create Profile" } diff --git a/app/views/enterprise_mailer/confirmation_instructions.html.haml b/app/views/enterprise_mailer/confirmation_instructions.html.haml index e957b70b1c..3fe7ba09e9 100644 --- a/app/views/enterprise_mailer/confirmation_instructions.html.haml +++ b/app/views/enterprise_mailer/confirmation_instructions.html.haml @@ -1,20 +1,22 @@ %h3 = "Hi, #{@resource.contact}!" %p.lead - = "Please confirm your email address for " - %strong - = "#{@resource.name}." + = "A profile for #{@resource.name} has been successfully created!" + To activate your Profile we need to confirm this email address. %p   %p.callout - Click the link below to confirm your email and to activate your enterprise. This link can be used only once: + Please click the link below to confirm your email and to continue setting up your profile. %br %strong = link_to 'Confirm this email address »', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %p   %p - = "We're so excited that you're joining the #{ Spree::Config[:site_name] }! Don't hestitate to get in touch if you have any questions." + After confirming your email you can access your administration account for this enterprise. + See the + = link_to 'User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' + = "to find out more about #{ Spree::Config[:site_name] }'s features and to start using your profile or online store." = render 'shared/mailers/signoff' diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index 5c69caae77..d24cc25904 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -1,67 +1,32 @@ %h3 = "Welcome, #{@enterprise.contact}!" %p.lead - Congratulations, + Thank you for confirming your email address. %strong - %strong= @enterprise.name + = @enterprise.name = "is now part of #{ Spree::Config.site_name }!" -/ Heading Panel + +%p + The User Guide with detailed support for setting up your Producer or Hub is here: + = link_to 'Open Food Network User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' + +%p + You can manage your account by logging into the + = link_to 'Admin Panel', spree.admin_url + or by clicking on the cog in the top right hand side of the homepage, and selecting Administration. + +%p + We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. + = link_to 'Join the community.', 'http://community.openfoodnetwork.org/' + %p Please find below all the details for viewing and editing your enterprise on %strong= "#{ Spree::Config.site_name }." We suggest keeping this email and information somewhere safe. Logging in with the account details below will allow complete access to your products and services. --#%p   - --# %p.callout --# %strong --# Your enterprise details --# %table{:width => "100%"} --# %tr --# %td{:align => "right"} --# %strong --# Shop URL --# %td   --# %td --# %a{:href => "#{ main_app.enterprise_shop_url(@enterprise) }", :target => "_blank"} --# = main_app.enterprise_shop_url(@enterprise) --# %tr --# %td   --# %tr --# %td{:align => "right"} --# %strong --# Email --# %td   --# %td --# %a{:href => "mailto:#{ @enterprise.email }", :target => "_blank"} --# = @enterprise.email - -%p   %p - Log into - %strong= "#{ Spree::Config.site_name } Admin" - in order to edit your enterprise details such as website and social media links, or to start adding products to your enterprise! - -%p.callout - %strong - OFN Admin -%table{ :width => "100%"} - %tr - %td{:align => "right"} - %strong - Admin - %td   - %td - %a{:href => "#{ spree.admin_url }", :target => "_blank"} - = spree.admin_url - -%p   -/ /Heading Panel -%p - We're so pleased to have you as a valued member of - %strong= "#{Spree::Config.site_name}!" - Don't hestitate to get in touch if you have any questions. + If you have any difficulties, check out our FAQs, browse the forum or post a 'Support' topic and someone will help you out! = render 'shared/mailers/signoff' -= render 'shared/mailers/social_and_contact' \ No newline at end of file += render 'shared/mailers/social_and_contact' diff --git a/app/views/spree/user_mailer/signup_confirmation.html.haml b/app/views/spree/user_mailer/signup_confirmation.html.haml index fb52f57119..c85b377fa4 100644 --- a/app/views/spree/user_mailer/signup_confirmation.html.haml +++ b/app/views/spree/user_mailer/signup_confirmation.html.haml @@ -21,7 +21,9 @@ %hr/ %p   %p.lead - Thanks for joining the network. We look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! + Thanks for joining the network. + If you are a customer, we look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! + If you are a producer or food enterprise, we are excited to have you as a part of the network. %p We welcome all your questions and feedback; you can use the %em From 50ae331d9459af558319558ea913b5a5562aaa5c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 29 May 2015 16:03:16 +1000 Subject: [PATCH 086/120] 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 087/120] 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 088/120] 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 cb2adea59f4db437edbe40cc5167682f45cbd405 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 3 Jun 2015 10:16:29 +1000 Subject: [PATCH 089/120] Remove executable bit from a migration file --- db/migrate/20120327000593_add_addresses_checkouts_indexes.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 db/migrate/20120327000593_add_addresses_checkouts_indexes.rb diff --git a/db/migrate/20120327000593_add_addresses_checkouts_indexes.rb b/db/migrate/20120327000593_add_addresses_checkouts_indexes.rb old mode 100755 new mode 100644 From 96177b7cd7092ed2c79abc23eacf953e2836c12e Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 3 Jun 2015 11:57:22 +1000 Subject: [PATCH 090/120] Add unique index to enterprise permalink --- ...3_add_unique_index_to_enterprise_permalink.rb | 16 ++++++++++++++++ db/schema.rb | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb diff --git a/db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb b/db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb new file mode 100644 index 0000000000..e8841b2c5f --- /dev/null +++ b/db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb @@ -0,0 +1,16 @@ +class AddUniqueIndexToEnterprisePermalink < ActiveRecord::Migration + def change + duplicates = Enterprise.group(:permalink).having('count(*) > 1').pluck(:permalink) + duplicates.each { |p| resolve_permalink(p) }; + add_index :enterprises, :permalink, :unique => true + end + + def resolve_permalink(permalink) + conflicting = Enterprise.where(permalink: permalink) + while conflicting.size > 1 do + enterprise = conflicting.pop + enterprise.permalink = nil + enterprise.save + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 6d9de020f1..bf6ecae939 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 => 20150527004427) do +ActiveRecord::Schema.define(:version => 20150603001843) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -323,6 +323,7 @@ ActiveRecord::Schema.define(:version => 20150527004427) do add_index "enterprises", ["confirmation_token"], :name => "index_enterprises_on_confirmation_token", :unique => true add_index "enterprises", ["is_primary_producer", "sells"], :name => "index_enterprises_on_is_primary_producer_and_sells" add_index "enterprises", ["owner_id"], :name => "index_enterprises_on_owner_id" + add_index "enterprises", ["permalink"], :name => "index_enterprises_on_permalink", :unique => true add_index "enterprises", ["sells"], :name => "index_enterprises_on_sells" create_table "exchange_fees", :force => true do |t| From 4b6222bbe2ccb20e0ebdb4852e145cf266b2bb3e Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 3 Jun 2015 12:08:47 +1000 Subject: [PATCH 091/120] Updating spec using new button label --- spec/features/consumer/registration_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index 25a91ae160..27b748cce5 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -45,7 +45,7 @@ feature "Registration", js: true do # Choosing a type expect(page).to have_content 'Last step to add My Awesome Enterprise!' click_link 'producer-panel' - click_button 'Continue' + click_button 'Create Profile' # Enterprise should be created expect(page).to have_content 'Nice one!' From 8afffdae9ab7d205475ede289213524c5ed14ac1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 12:13:42 +1000 Subject: [PATCH 092/120] Fix error when product does not have a master variant --- app/assets/javascripts/darkswarm/services/products.js.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/products.js.coffee b/app/assets/javascripts/darkswarm/services/products.js.coffee index a07ae1f466..4785adae85 100644 --- a/app/assets/javascripts/darkswarm/services/products.js.coffee +++ b/app/assets/javascripts/darkswarm/services/products.js.coffee @@ -32,8 +32,9 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Pro if product.variants product.variants = (Variants.register variant for variant in product.variants) variant.product = product for variant in product.variants - product.master.product = product - product.master = Variants.register product.master if product.master + if product.master + product.master.product = product + product.master = Variants.register product.master registerVariantsWithCart: -> for product in @products From 473322c7e6b14a4c24927bff36440364d9138206 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 12:25:28 +1000 Subject: [PATCH 093/120] CI: Add more robust merge-to-master script --- script/ci/merge_branch_to_master.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/script/ci/merge_branch_to_master.sh b/script/ci/merge_branch_to_master.sh index 3eaae17d56..fd9c602802 100755 --- a/script/ci/merge_branch_to_master.sh +++ b/script/ci/merge_branch_to_master.sh @@ -6,5 +6,9 @@ source ./script/ci/includes.sh echo "--- Verifying branch is based on current master" exit_unless_master_merged -echo "--- Pushing branch" -git push origin $BUILDKITE_COMMIT:master +echo "--- Merging and pushing branch" +git checkout master +git merge origin/master +git merge origin/$BUILDKITE_BRANCH +git push origin master +git checkout origin/$BUILDKITE_BRANCH From c6f6c11a43143d3c820be511b8c940937696c49a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 12:51:15 +1000 Subject: [PATCH 094/120] Add wait between clicks to fix race condition --- spec/features/admin/bulk_product_update_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 4c52ec2256..f5134f4e81 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -205,8 +205,9 @@ feature %q{ expect(page).to have_selector "a.edit-variant", count: 1 # When I remove two, they should be removed - page.all('a.delete-variant').first.click - page.all('a.delete-variant').first.click + page.all('a.delete-variant', visible: true).first.click + expect(page).to have_selector "tr.variant", count: 2 + page.all('a.delete-variant', visible: true).first.click expect(page).to have_selector "tr.variant", count: 1 # When I fill out variant details and hit update From b3878b126b3c73b6baa1cefd35feb06e6813ce7a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 12:53:46 +1000 Subject: [PATCH 095/120] Decouple generic injection spec from EnterpriseSerializer --- spec/helpers/injection_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/injection_helper_spec.rb b/spec/helpers/injection_helper_spec.rb index 96d9279ef5..362ca6479b 100644 --- a/spec/helpers/injection_helper_spec.rb +++ b/spec/helpers/injection_helper_spec.rb @@ -4,7 +4,7 @@ describe InjectionHelper do let!(:enterprise) { create(:distributor_enterprise, facebook: "roger") } it "will inject via AMS" do - helper.inject_json_ams("test", [enterprise], Api::EnterpriseSerializer).should match enterprise.name + helper.inject_json_ams("test", [enterprise], Api::IdSerializer).should match /#{enterprise.id}/ end it "injects enterprises" do From 36dc0d5ccd767446a63c9b4dd07bfb4f41a41c5b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 13:00:07 +1000 Subject: [PATCH 096/120] Do not run performance specs in CI --- script/ci/run_tests.sh | 2 +- spec/performance/injection_helper_spec.rb | 2 +- spec/performance/shop_controller_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/script/ci/run_tests.sh b/script/ci/run_tests.sh index 5539935a8b..f973c139ad 100755 --- a/script/ci/run_tests.sh +++ b/script/ci/run_tests.sh @@ -16,4 +16,4 @@ echo "--- Loading test database" bundle exec rake db:test:load echo "--- Running tests" -bundle exec rspec spec +bundle exec rspec --tag ~performance spec diff --git a/spec/performance/injection_helper_spec.rb b/spec/performance/injection_helper_spec.rb index 4ba22d738f..ef8d937ce1 100644 --- a/spec/performance/injection_helper_spec.rb +++ b/spec/performance/injection_helper_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe InjectionHelper, type: :helper do +describe InjectionHelper, type: :helper, performance: true do let(:oc) { create(:simple_order_cycle) } let(:relative_supplier) { create(:supplier_enterprise) } let(:relative_distributor) { create(:distributor_enterprise) } diff --git a/spec/performance/shop_controller_spec.rb b/spec/performance/shop_controller_spec.rb index 14e258d18d..984581a2ab 100644 --- a/spec/performance/shop_controller_spec.rb +++ b/spec/performance/shop_controller_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ShopController, type: :controller do +describe ShopController, type: :controller, performance: true do let(:d) { create(:distributor_enterprise) } let(:enterprise_fee) { create(:enterprise_fee) } let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator_fees: [enterprise_fee]) } From db47c01784df955cff00560f3c4b5454467025dd Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 1 May 2015 09:50:12 +1000 Subject: [PATCH 097/120] 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 098/120] 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 099/120] 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 17d123db63643399a3a24a5135f77e879090927b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 16:28:43 +1000 Subject: [PATCH 100/120] ng-cloak producers page --- app/views/producers/index.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/producers/index.html.haml b/app/views/producers/index.html.haml index 0e46795701..e3ae9815e2 100644 --- a/app/views/producers/index.html.haml +++ b/app/views/producers/index.html.haml @@ -1,5 +1,6 @@ -= inject_enterprises -.producers.pad-top{"ng-controller" => "EnterprisesCtrl"} += inject_enterprises + +.producers.pad-top{"ng-controller" => "EnterprisesCtrl", "ng-cloak" => true} .row .small-12.columns.pad-top %h1 Find local producers From 0569ef05050b9f0bb8851f51ff7bd2c4f56cb5ba Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 16:36:31 +1000 Subject: [PATCH 101/120] Only record analytics in production. Also record them in admin backend. --- .../layouts/admin/add_analytics.html.haml.deface | 3 +++ app/views/shared/_analytics.html.haml | 15 ++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 app/overrides/spree/layouts/admin/add_analytics.html.haml.deface diff --git a/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface b/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface new file mode 100644 index 0000000000..548439b60f --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_bottom "[data-hook='admin_footer_scripts']" + += render 'shared/analytics' diff --git a/app/views/shared/_analytics.html.haml b/app/views/shared/_analytics.html.haml index ee9ba69923..16ad08ff5f 100644 --- a/app/views/shared/_analytics.html.haml +++ b/app/views/shared/_analytics.html.haml @@ -1,8 +1,9 @@ -:javascript - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); +- if Rails.env.production? + :javascript + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - ga('create', 'UA-62912229-1', 'auto'); - ga('send', 'pageview'); + ga('create', 'UA-62912229-1', 'auto'); + ga('send', 'pageview'); From 2c0da5e3509ed58b0084e1e539c6082c1c942472 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 16:38:47 +1000 Subject: [PATCH 102/120] Output parallel spec runtime log -> evenly split spec grouping --- .rspec_parallel | 1 + 1 file changed, 1 insertion(+) diff --git a/.rspec_parallel b/.rspec_parallel index 590f731dd1..867e417e06 100644 --- a/.rspec_parallel +++ b/.rspec_parallel @@ -1,3 +1,4 @@ --format progress --format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log +--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log --tag ~performance From f1019e9221c5ba53470e85e097c2f64d02101199 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Jun 2015 16:53:46 +1000 Subject: [PATCH 103/120] Upgrade unicorn --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 09c5f866f3..621c705d49 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -326,7 +326,7 @@ GEM kaminari (0.14.1) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - kgio (2.7.4) + kgio (2.9.3) launchy (2.1.2) addressable (~> 2.3) letter_opener (1.0.0) @@ -416,7 +416,7 @@ GEM rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - raindrops (0.9.0) + raindrops (0.13.0) rake (10.4.2) ransack (0.7.2) actionpack (~> 3.0) @@ -503,7 +503,7 @@ GEM uglifier (1.2.4) execjs (>= 0.3.0) multi_json (>= 1.0.2) - unicorn (4.3.1) + unicorn (4.9.0) kgio (~> 2.6) rack raindrops (~> 0.7) From 09160c8ea67768afeca2c54a186586b29da4796e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 3 Jun 2015 15:23:55 +0800 Subject: [PATCH 104/120] 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 105/120] 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 106/120] 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 107/120] 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 108/120] 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 109/120] 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 From 85d1d67daccb841280b87ab08e84a84da4efd7bd Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Jun 2015 09:42:03 +1000 Subject: [PATCH 110/120] Sort orders as required by spec, fixes intermittent fail --- 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..e5f831a41c 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 ASC').ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) render json: @orders, each_serializer: Api::Admin::OrderSerializer end end From 552bbf221c0c799c07fe80e0bc5901c625c68dcd Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 5 Jun 2015 10:59:39 +1000 Subject: [PATCH 111/120] Delete obsolete paragraph in registration #595 --- app/views/enterprise_mailer/welcome.html.haml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index d24cc25904..3cd9d14034 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -19,11 +19,6 @@ We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. = link_to 'Join the community.', 'http://community.openfoodnetwork.org/' -%p - Please find below all the details for viewing and editing your enterprise on - %strong= "#{ Spree::Config.site_name }." - We suggest keeping this email and information somewhere safe. Logging in with the account details below will allow complete access to your products and services. - %p If you have any difficulties, check out our FAQs, browse the forum or post a 'Support' topic and someone will help you out! From e35b39c7cfbbe6750acb7b152ed7b85bac3790c1 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 5 Jun 2015 12:43:49 +1000 Subject: [PATCH 112/120] Handle invalid referer URLs Rescues URI::InvalidURIError of URL(request.referer). --- .../admin/enterprises_controller.rb | 5 +++- app/controllers/application_controller.rb | 5 +++- .../admin/products_controller_decorator.rb | 4 +++- lib/open_food_network/referer_parser.rb | 17 ++++++++++++++ .../open_food_network/referer_parser_spec.rb | 23 +++++++++++++++++++ 5 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 lib/open_food_network/referer_parser.rb create mode 100644 spec/lib/open_food_network/referer_parser_spec.rb diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index a0b55ad3c8..e50779c3ff 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -1,3 +1,5 @@ +require 'open_food_network/referer_parser' + module Admin class EnterprisesController < ResourceController before_filter :load_enterprise_set, :only => :index @@ -199,7 +201,8 @@ module Admin # Overriding method on Spree's resource controller def location_after_save - refered_from_edit = URI(request.referer).path == main_app.edit_admin_enterprise_path(@enterprise) + referer_path = OpenFoodNetwork::RefererParser::path(request.referer) + refered_from_edit = referer_path == main_app.edit_admin_enterprise_path(@enterprise) if params[:enterprise].key?(:producer_properties_attributes) && !refered_from_edit main_app.admin_enterprises_path else diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 53763ad274..aaa7d0bb06 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +require 'open_food_network/referer_parser' + class ApplicationController < ActionController::Base protect_from_forgery @@ -9,7 +11,8 @@ class ApplicationController < ActionController::Base end def set_checkout_redirect - if request.referer and referer_path = URI(request.referer).path + referer_path = OpenFoodNetwork::RefererParser::path(request.referer) + if referer_path session["spree_user_return_to"] = [main_app.checkout_path].include?(referer_path) ? referer_path : root_path end end diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 1591586f76..5b1bb347a1 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -1,4 +1,5 @@ require 'open_food_network/spree_api_key_loader' +require 'open_food_network/referer_parser' Spree::Admin::ProductsController.class_eval do include OpenFoodNetwork::SpreeApiKeyLoader @@ -53,7 +54,8 @@ Spree::Admin::ProductsController.class_eval do protected def location_after_save - if URI(request.referer).path == '/admin/products/bulk_edit' + referer_path = OpenFoodNetwork::RefererParser::path(request.referer) + if referer_path == '/admin/products/bulk_edit' bulk_edit_admin_products_url else location_after_save_original diff --git a/lib/open_food_network/referer_parser.rb b/lib/open_food_network/referer_parser.rb new file mode 100644 index 0000000000..b90ef21829 --- /dev/null +++ b/lib/open_food_network/referer_parser.rb @@ -0,0 +1,17 @@ +module OpenFoodNetwork + class RefererParser + def self.path(referer) + parse_uri(referer).andand.path if referer + end + + def self.parse_uri(string) + begin + # TODO: make this operation obsolete by fixing URLs generated by AngularJS + string.sub!('##', '#') + URI(string) + rescue URI::InvalidURIError + nil + end + end + end +end diff --git a/spec/lib/open_food_network/referer_parser_spec.rb b/spec/lib/open_food_network/referer_parser_spec.rb new file mode 100644 index 0000000000..13cde6099e --- /dev/null +++ b/spec/lib/open_food_network/referer_parser_spec.rb @@ -0,0 +1,23 @@ +require 'open_food_network/referer_parser' +require 'spec_helper' + +module OpenFoodNetwork + describe RefererParser do + + it "handles requests without referer" do + RefererParser.path(nil).should be_nil + end + + it "handles requests with referer" do + RefererParser.path('http://example.org/').should eq('/') + end + + it "handles requests with invalid referer" do + RefererParser.path('this is not a URI').should be_nil + end + + it "handles requests with known issue of referer" do + RefererParser.path('http://example.org/##invalid-fragment').should eq('/') + end + end +end From 41dafce0db5ef155fd8fd25c34ffbec0159df88a Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 4 Jun 2015 16:04:49 +1000 Subject: [PATCH 113/120] Remove obsolete Bugsnag notification --- app/models/spree/user_decorator.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index d8ea312e23..13ab56c129 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -1,9 +1,7 @@ Spree.user_class.class_eval do - if method_defined? :send_reset_password_instructions_with_delay - Bugsnag.notify RuntimeError.new "send_reset_password_instructions already handled asyncronously - double-calling results in infinite job loop" - else - handle_asynchronously :send_reset_password_instructions - end + # handle_asynchronously will define send_reset_password_instructions_with_delay. + # If handle_asynchronously is called twice, we get an infinite job loop. + handle_asynchronously :send_reset_password_instructions unless method_defined? :send_reset_password_instructions_with_delay has_many :enterprise_roles, :dependent => :destroy has_many :enterprises, through: :enterprise_roles From d6c630dad91df4e2072570c13dbd05ba81407481 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 5 Jun 2015 14:25:45 +1000 Subject: [PATCH 114/120] add spec_helper to customers_controller_spec --- spec/controllers/admin/customers_controller_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/controllers/admin/customers_controller_spec.rb b/spec/controllers/admin/customers_controller_spec.rb index 3fc5451b8b..bb2e4888c2 100644 --- a/spec/controllers/admin/customers_controller_spec.rb +++ b/spec/controllers/admin/customers_controller_spec.rb @@ -1,3 +1,5 @@ +require 'spec_helper' + describe Admin::CustomersController, type: :controller do include AuthenticationWorkflow From 153360d17b6545d6d23a2a850423156f8f8ecfbc Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 5 Jun 2015 16:10:58 +1000 Subject: [PATCH 115/120] Let the database delete adjustment metadata GitHub issue #582 We encountered a foreign key violation on adjustment metadata even though rails should handle that. Action: order.adjustments.where(originator_type: 'EnterpriseFee').destroy_all Adjustment: has_one :metadata, class_name: 'AdjustmentMetadata' dependent: :destroy So we update the foreign key to cause a delete in the database. --- app/models/spree/adjustment_decorator.rb | 5 ++++- ...0605052516_dependent_delete_adjustment_metadata.rb | 11 +++++++++++ db/schema.rb | 4 ++-- 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20150605052516_dependent_delete_adjustment_metadata.rb diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb index 836080183c..b6f3bcb15c 100644 --- a/app/models/spree/adjustment_decorator.rb +++ b/app/models/spree/adjustment_decorator.rb @@ -1,6 +1,9 @@ module Spree Adjustment.class_eval do - has_one :metadata, class_name: 'AdjustmentMetadata', dependent: :destroy + # Deletion of metadata is handled in the database. + # So we don't need the option `dependent: :destroy` as long as + # AdjustmentMetadata has no destroy logic itself. + has_one :metadata, class_name: 'AdjustmentMetadata' scope :enterprise_fee, where(originator_type: 'EnterpriseFee') scope :included_tax, where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem') diff --git a/db/migrate/20150605052516_dependent_delete_adjustment_metadata.rb b/db/migrate/20150605052516_dependent_delete_adjustment_metadata.rb new file mode 100644 index 0000000000..ee9d8d4af8 --- /dev/null +++ b/db/migrate/20150605052516_dependent_delete_adjustment_metadata.rb @@ -0,0 +1,11 @@ +class DependentDeleteAdjustmentMetadata < ActiveRecord::Migration + def up + remove_foreign_key "adjustment_metadata", name: "adjustment_metadata_adjustment_id_fk" + add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id", dependent: :delete + end + + def down + remove_foreign_key "adjustment_metadata", name: "adjustment_metadata_adjustment_id_fk" + add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id" + end +end diff --git a/db/schema.rb b/db/schema.rb index f6cf50a8fa..e8de57b170 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 => 20150527004427) do +ActiveRecord::Schema.define(:version => 20150605052516) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -1115,7 +1115,7 @@ ActiveRecord::Schema.define(:version => 20150527004427) do add_index "variant_overrides", ["variant_id", "hub_id"], :name => "index_variant_overrides_on_variant_id_and_hub_id" add_foreign_key "adjustment_metadata", "enterprises", name: "adjustment_metadata_enterprise_id_fk" - add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id" + add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id", dependent: :delete add_foreign_key "carts", "spree_users", name: "carts_user_id_fk", column: "user_id" From 88872b4b4930a1b2602bcddff6647cba9465d033 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 5 Jun 2015 14:25:45 +1000 Subject: [PATCH 116/120] add spec_helper to customers_controller_spec --- spec/controllers/admin/customers_controller_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/controllers/admin/customers_controller_spec.rb b/spec/controllers/admin/customers_controller_spec.rb index 3fc5451b8b..bb2e4888c2 100644 --- a/spec/controllers/admin/customers_controller_spec.rb +++ b/spec/controllers/admin/customers_controller_spec.rb @@ -1,3 +1,5 @@ +require 'spec_helper' + describe Admin::CustomersController, type: :controller do include AuthenticationWorkflow From f88f42283a28a9f80659fab50e3677019e2a2240 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 4 Jun 2015 15:10:41 +1000 Subject: [PATCH 117/120] Session stored in ActiveRecored instead of Cookies The cookie store is not big enough in some cases. In order to solve a CookieOverflow error and maybe track down the underlying issue this patch uses the database instead of cookies to store session data. --- config/initializers/session_store.rb | 6 ++++-- db/migrate/20150604045725_add_sessions_table.rb | 12 ++++++++++++ db/schema.rb | 12 +++++++++++- spec/requests/large_request_spec.rb | 11 +++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20150604045725_add_sessions_table.rb create mode 100644 spec/requests/large_request_spec.rb diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 721e495265..d301b88739 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,8 +1,10 @@ # Be sure to restart your server when you modify this file. -Openfoodnetwork::Application.config.session_store :cookie_store, key: '_openfoodnetwork_session' +# The cookie_store can be too small for very long URLs stored by Devise. +# The maximum size of cookies is 4096 bytes. +#Openfoodnetwork::Application.config.session_store :cookie_store, key: '_openfoodnetwork_session' # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rails generate session_migration") -# Openfoodnetwork::Application.config.session_store :active_record_store +Openfoodnetwork::Application.config.session_store :active_record_store diff --git a/db/migrate/20150604045725_add_sessions_table.rb b/db/migrate/20150604045725_add_sessions_table.rb new file mode 100644 index 0000000000..4c879564a5 --- /dev/null +++ b/db/migrate/20150604045725_add_sessions_table.rb @@ -0,0 +1,12 @@ +class AddSessionsTable < ActiveRecord::Migration + def change + create_table :sessions do |t| + t.string :session_id, :null => false + t.text :data + t.timestamps + end + + add_index :sessions, :session_id + add_index :sessions, :updated_at + end +end diff --git a/db/schema.rb b/db/schema.rb index e58d74c220..51fe2cd0e4 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 => 20150603001843) do +ActiveRecord::Schema.define(:version => 20150604045725) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -397,6 +397,16 @@ ActiveRecord::Schema.define(:version => 20150603001843) do add_index "product_distributions", ["enterprise_fee_id"], :name => "index_product_distributions_on_enterprise_fee_id" add_index "product_distributions", ["product_id"], :name => "index_product_distributions_on_product_id" + create_table "sessions", :force => true do |t| + t.string "session_id", :null => false + t.text "data" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id" + add_index "sessions", ["updated_at"], :name => "index_sessions_on_updated_at" + create_table "spree_activators", :force => true do |t| t.string "description" t.datetime "expires_at" diff --git a/spec/requests/large_request_spec.rb b/spec/requests/large_request_spec.rb new file mode 100644 index 0000000000..16fa4ce679 --- /dev/null +++ b/spec/requests/large_request_spec.rb @@ -0,0 +1,11 @@ +# Large requests can fail if Devise tries to store the URL in the session cookie. +# +# http://daniel.fone.net.nz/blog/2014/11/28/actiondispatch-cookies-cookieoverflow-via-devise-s-user_return_to/ +require 'spec_helper' + +RSpec.describe 'A very large request', type: :request do + it 'should not overflow cookies' do + get '/admin', foo: 'x' * ActionDispatch::Cookies::SignedCookieJar::MAX_COOKIE_SIZE + expect(response).to redirect_to 'http://www.example.com/#login?after_login=/admin' + end +end From 6c0238deacc7ceda7915b25ae2a09adfa65f1518 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Sat, 6 Jun 2015 13:26:21 +1000 Subject: [PATCH 118/120] scoping down large request spec --- spec/requests/large_request_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/requests/large_request_spec.rb b/spec/requests/large_request_spec.rb index 16fa4ce679..6e389d5ecb 100644 --- a/spec/requests/large_request_spec.rb +++ b/spec/requests/large_request_spec.rb @@ -6,6 +6,8 @@ require 'spec_helper' RSpec.describe 'A very large request', type: :request do it 'should not overflow cookies' do get '/admin', foo: 'x' * ActionDispatch::Cookies::SignedCookieJar::MAX_COOKIE_SIZE - expect(response).to redirect_to 'http://www.example.com/#login?after_login=/admin' + expect(response.status).to eq(302) # HTTP status 302 - Found + ## Use the newer syntax if rspec gets upgraded + # expect(response).to have_http_status(:redirect) end end From 5a7231579c935ce80208c0c72f45bb79f9b6e115 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 10 Jun 2015 13:58:52 +1000 Subject: [PATCH 119/120] update uglifier to 2.7.1 --- Gemfile.lock | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index eddc27ee84..b7f8f449b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -251,8 +251,7 @@ GEM erubis (2.7.0) eventmachine (1.0.3) excon (0.25.3) - execjs (1.4.0) - multi_json (~> 1.0) + execjs (2.5.2) factory_girl (3.3.0) activesupport (>= 3.0.0) factory_girl_rails (3.3.0) @@ -502,9 +501,9 @@ GEM turn (0.8.3) ansi tzinfo (0.3.44) - uglifier (1.2.4) + uglifier (2.7.1) execjs (>= 0.3.0) - multi_json (>= 1.0.2) + json (>= 1.8.0) unicorn (4.9.0) kgio (~> 2.6) rack From a8b6f2942a31c9218cb14cf66d6cd6fbec8fcee0 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 11 Jun 2015 10:54:30 +1000 Subject: [PATCH 120/120] Releasing Assets Version 1.1 The uglifier update affects only re-compiled assets. In order to replace the broken assets (e.g. #396) with re-compiled assets, we change the assets version number. --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index 2d9dcd4982..ef0bf028be 100644 --- a/config/application.rb +++ b/config/application.rb @@ -79,7 +79,7 @@ module Openfoodnetwork config.assets.enabled = true # Version of your assets, change this if you want to expire all your assets - config.assets.version = '1.0' + config.assets.version = '1.1' config.sass.load_paths += [ "#{Gem.loaded_specs['foundation-rails'].full_gem_path}/vendor/assets/stylesheets/foundation/components",