From 90eea5cb16937f7490ac5ab2706becd1539bc0eb Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 29 Jul 2014 15:23:10 +1000 Subject: [PATCH 001/205] Switching to a single directive to render variants on /shop --- .../directives/shop_variant.js.coffee | 6 ++ .../templates/shop_variant.html.haml} | 13 ++--- app/views/shop/products/_form.html.haml | 8 +-- app/views/shop/products/_master.html.haml | 58 ------------------- 4 files changed, 14 insertions(+), 71 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/directives/shop_variant.js.coffee rename app/{views/shop/products/_variants.html.haml => assets/javascripts/templates/shop_variant.html.haml} (83%) delete mode 100644 app/views/shop/products/_master.html.haml diff --git a/app/assets/javascripts/darkswarm/directives/shop_variant.js.coffee b/app/assets/javascripts/darkswarm/directives/shop_variant.js.coffee new file mode 100644 index 0000000000..e4cbed11c5 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/shop_variant.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.directive "shopVariant", -> + restrict: 'E' + replace: true + templateUrl: 'shop_variant.html' + scope: + variant: '=' diff --git a/app/views/shop/products/_variants.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml similarity index 83% rename from app/views/shop/products/_variants.html.haml rename to app/assets/javascripts/templates/shop_variant.html.haml index 3427d23a5f..d0eb4b67c7 100644 --- a/app/views/shop/products/_variants.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -1,16 +1,15 @@ -.row.variants{bindonce: true, - "ng-repeat" => "variant in product.variants track by variant.id"} - +.variants.row .small-12.medium-4.large-4.columns.variant-name .table-cell .inline {{ variant.name_to_display }} - .bulk-buy.inline{"bo-if" => "product.group_buy"} + .bulk-buy.inline{"bo-if" => "variant.product.group_buy"} %i.ofn-i_056-bulk>< %em>< \ Bulk -# WITHOUT GROUP BUY - .small-5.medium-3.large-3.columns.text-right{"bo-if" => "!product.group_buy"} + .small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} + %input{type: :number, value: nil, min: 0, @@ -22,7 +21,7 @@ -# WITH GROUP BUY - .small-5.medium-3.large-3.columns.text-right{"bo-if" => "product.group_buy"} + .small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"} %span.bulk-input-container %span.bulk-input %input.bulk.first{type: :number, @@ -33,7 +32,7 @@ "ofn-disable-scroll" => true, max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} - %span.bulk-input{"bo-if" => "product.group_buy"} + %span.bulk-input{"bo-if" => "variant.product.group_buy"} %input.bulk.second{type: :number, min: 0, "ng-model" => "variant.line_item.max_quantity", diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 25004f97b4..a10edd7fb9 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -20,12 +20,8 @@ "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | orderBy:ordering.order) track by product.id "} = render partial: "shop/products/summary" - - %span{"bo-if" => "product.hasVariants"} - = render partial: "shop/products/variants" - - .variants.row{"bo-if" => "!product.hasVariants"} - = render partial: "shop/products/master" + %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants"} + %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id"} %product{"ng-show" => "Products.loading"} .row.summary diff --git a/app/views/shop/products/_master.html.haml b/app/views/shop/products/_master.html.haml deleted file mode 100644 index f12df1b881..0000000000 --- a/app/views/shop/products/_master.html.haml +++ /dev/null @@ -1,58 +0,0 @@ -.small-12.medium-4.large-4.columns.variant-name - .table-cell - .inline {{ product.master.name_to_display }} - .bulk-buy.inline{"bo-if" => "product.group_buy"} - %i.ofn-i_056-bulk>< - %em>< - \ Bulk - --# WITHOUT GROUP BUY -.small-5.medium-3.large-3.columns.text-right{"bo-if" => "!product.group_buy"} - %input{type: :number, - min: 0, - placeholder: "0", - "ofn-disable-scroll" => true, - max: "{{product.on_demand && 9999 || product.count_on_hand }}", - name: "variants[{{product.master.id}}]", - "ng-model" => "product.master.line_item.quantity", - id: "variants_{{product.master.id}}"} - --# WITH GROUP BUY -.small-5.medium-3.large-3.columns.text-right{"bo-if" => "product.group_buy"} - %span.bulk-input-container - %span.bulk-input - %input.bulk.first{type: :number, - min: 0, - "ng-model" => "product.master.line_item.quantity", - placeholder: "min", - "ofn-disable-scroll" => true, - max: "{{product.on_demand && 9999 || product.count_on_hand }}", - name: "variants[{{product.master.id}}]", - id: "variants_{{product.master.id}}"} - - %span.bulk-input{"bo-if" => "product.group_buy"} - %input.bulk.second{type: :number, - min: 0, - "ng-model" => "product.master.line_item.max_quantity", - placeholder: "max", - "ofn-disable-scroll" => true, - max: "{{product.on_demand && 9999 || product.count_on_hand }}", - name: "variant_attributes[{{product.master.id}}][max_quantity]"} - -.small-3.medium-1.large-1.columns.variant-unit - .table-cell - %em {{ product.master.unit_to_display }} - -.small-4.medium-2.large-2.columns.variant-price - .table-cell - %i.ofn-i_009-close - {{ product.master.price | currency }} - -#%button.graph-button{"price-breakdown" => "_", - -#"variant" => "product.master", - -#"price-breakdown-animation" => "true"} - -#%i.ofn-i-058-graph - -.small-12.medium-2.large-2.columns.total-price.text-right - .table-cell - %strong - {{ product.master.getPrice() | currency }} From 6873b33e1f3fb20c8e706345c700cbc70bfa6033 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 29 Jul 2014 15:32:19 +1000 Subject: [PATCH 002/205] Directive restored --- app/assets/javascripts/templates/shop_variant.html.haml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index d0eb4b67c7..938259d80f 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -50,8 +50,10 @@ %i.ofn-i_009-close {{ variant.price | currency }} - / %button.graph-button{popover: "This is the popover text", "popover-title" => "The title.", "popover-animation" => "true", "popover-trigger" =>"mouseenter", "popover-placement" => "top", "tabindex" => "-1"} - / %i.ofn-i-058-graph + %button.graph-button{"price-breakdown" => "_", + "variant" => "variant", + "price-breakdown-animation" => "true"} + %i.ofn-i-058-graph .small-12.medium-2.large-2.columns.total-price.text-right .table-cell From 682b04287e98a5d0f363d0fa3b9c398ac6d9fbf0 Mon Sep 17 00:00:00 2001 From: summerscope Date: Tue, 29 Jul 2014 16:03:13 +1000 Subject: [PATCH 003/205] Make product thumbnail background white by default --- app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass index 8bc1550d54..7a69707a60 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass @@ -13,7 +13,7 @@ float: left display: block z-index: 999999 - background-color: #999 + background-color: white overflow: hidden @media all and (max-width: 768px) From 62d5149e7a0c515e615c65bbbe3dbeb74dcf9ddf Mon Sep 17 00:00:00 2001 From: summerscope Date: Tue, 29 Jul 2014 16:15:35 +1000 Subject: [PATCH 004/205] Add back styling which sort of works for modals until i can pair with will to fix it --- app/assets/stylesheets/darkswarm/modals.css.sass | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index 182ab5bec0..77f0b2fb54 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -5,6 +5,8 @@ dialog, .reveal-modal border: none outline: none padding: 1rem + + // TO DO: deal with scroll container inheritance (kirsten box) + bigger scrolling on mobile device issue div overflow: scroll @media only screen and (min-width: 40.063em) From 8ed79c6e55ba1faa7c7b84b846eec2cdbcc155d9 Mon Sep 17 00:00:00 2001 From: summerscope Date: Tue, 29 Jul 2014 16:50:26 +1000 Subject: [PATCH 005/205] Make scrolling on outer container only --- .../stylesheets/darkswarm/modals.css.sass | 79 +++++++++++-------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index 77f0b2fb54..086ab8b076 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -5,47 +5,56 @@ dialog, .reveal-modal border: none outline: none padding: 1rem - - // TO DO: deal with scroll container inheritance (kirsten box) + bigger scrolling on mobile device issue - div - overflow: scroll - @media only screen and (min-width: 40.063em) - max-height: 580px - @media all and (max-width: 768px) - max-height: 440px - @media all and (max-width: 640px) - max-height: 400px - @media all and (max-width: 640px) - max-height: inherit - overflow: scroll + // TO DO: look at bigger issue scrolling on mobile device + overflow-y: scroll + @media only screen and (min-width: 40.063em) + max-height: 580px + @media all and (max-width: 768px) + max-height: 440px + @media all and (max-width: 640px) + max-height: 400px + @media all and (max-width: 640px) + max-height: inherit + overflow-y: scroll .reveal-modal-bg background-color: rgba(0,0,0,0.65) -dialog .close-reveal-modal.outside, .reveal-modal .close-reveal-modal.outside - top: -2.5rem - right: -2.5rem - font-size: 2rem - color: white +dialog .close-reveal-modal, .reveal-modal .close-reveal-modal + top: 0.45rem + right: 0.4rem + background-color: rgba(235,235,235,0.85) text-shadow: none - padding: 0.25rem + padding: 0.3rem @include border-radius(999999) - border: 1px solid transparent &:hover, &:active, &:focus - text-shadow: 0 1px 3px #333 - border: 1px solid white + background-color: rgba(235,235,235,1) + color: #333 - @media all and (max-width: 640px) - top: 0.5rem - right: 0.5rem - font-size: 2rem - color: white - text-shadow: none - padding: 0.25rem - background-color: rgba(150,150,150,0.85) - @include border-radius(999999) - border: 1px solid transparent - &:hover, &:active, &:focus - text-shadow: 0 1px 3px #333 - border: 1px solid white +// dialog .close-reveal-modal.outside, .reveal-modal .close-reveal-modal.outside +// top: -2.5rem +// right: -2.5rem +// font-size: 2rem +// color: white +// text-shadow: none +// padding: 0.25rem +// @include border-radius(999999) +// border: 1px solid transparent +// &:hover, &:active, &:focus +// text-shadow: 0 1px 3px #333 +// border: 1px solid white + +// @media all and (max-width: 640px) +// top: 0.5rem +// right: 0.5rem +// font-size: 2rem +// color: white +// text-shadow: none +// padding: 0.25rem +// background-color: rgba(150,150,150,0.85) +// @include border-radius(999999) +// border: 1px solid transparent +// &:hover, &:active, &:focus +// text-shadow: 0 1px 3px #333 +// border: 1px solid white From 47a38d1815d3ebc35f8b5cc322c6170e59138b27 Mon Sep 17 00:00:00 2001 From: summerscope Date: Tue, 29 Jul 2014 16:50:48 +1000 Subject: [PATCH 006/205] Tweak the styling on producer modals so no grey background --- app/assets/stylesheets/darkswarm/images.css.sass | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/images.css.sass b/app/assets/stylesheets/darkswarm/images.css.sass index 39a86a93d1..ce205f0dae 100644 --- a/app/assets/stylesheets/darkswarm/images.css.sass +++ b/app/assets/stylesheets/darkswarm/images.css.sass @@ -11,9 +11,9 @@ @include box-shadow(0 1px 2px 1px rgba(0,0,0,0.25)) .hero-img - background-color: #333 + border-bottom: 1px solid $disabled-bright width: 100% - min-height: 160px + min-height: 56px height: inherit max-height: 260px overflow: hidden From cf1cf085d93cc5e0ce448038f28a036795edcf15 Mon Sep 17 00:00:00 2001 From: summerscope Date: Tue, 29 Jul 2014 17:08:31 +1000 Subject: [PATCH 007/205] Popover for shopping cart styling required for medium and small views --- app/assets/stylesheets/darkswarm/shopping-cart.css.sass | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass index 745dceb70f..3a63bb7a2e 100644 --- a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass +++ b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass @@ -13,13 +13,20 @@ right: 10px top: 55px width: 400px + @media screen and (max-width: 640px) + width: 96% .joyride-nub right: 22px !important left: auto + ul, li + list-style: none + margin-left: 0 + li float: none + .row .columns padding-left: 0.25rem padding-right: 0.25rem From 1ac5b79aeb0832e577534725c3826336c24f0ca0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 30 Jul 2014 14:33:36 +1000 Subject: [PATCH 008/205] Fix failing JS specs --- .../javascripts/darkswarm/services/products.js.coffee | 2 +- config/ng-test.conf.js | 1 - spec/javascripts/application_spec.js | 1 + .../controllers/products_controller_spec.js.coffee | 3 ++- .../darkswarm/filters/filter_groups_spec.js.coffee | 4 ++-- .../unit/darkswarm/filters/strip_url_spec.js.coffee | 11 ++++------- .../unit/darkswarm/services/product_spec.js.coffee | 7 ++++++- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/products.js.coffee b/app/assets/javascripts/darkswarm/services/products.js.coffee index cc4ffbf259..aec7b1cc3e 100644 --- a/app/assets/javascripts/darkswarm/services/products.js.coffee +++ b/app/assets/javascripts/darkswarm/services/products.js.coffee @@ -44,5 +44,5 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Car product.price = Math.min.apply(null, prices) product.hasVariants = product.variants?.length > 0 - product.primaryImage = product.images[0]?.small_url + product.primaryImage = product.images[0]?.small_url if product.images product.primaryImageOrMissing = product.primaryImage || "/assets/noimage/small.png" diff --git a/config/ng-test.conf.js b/config/ng-test.conf.js index 33f95483cf..eadaf984ae 100644 --- a/config/ng-test.conf.js +++ b/config/ng-test.conf.js @@ -8,7 +8,6 @@ module.exports = function(config) { APPLICATION_SPEC, 'app/assets/javascripts/shared/jquery-1.8.0.js', // TODO: Can we link to Rails' jquery? 'app/assets/javascripts/shared/jquery.timeago.js', - 'app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js', 'app/assets/javascripts/shared/angular-local-storage.js', 'app/assets/javascripts/shared/bindonce.min.js', 'app/assets/javascripts/shared/ng-infinite-scroll.min.js', diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index 44654e7b32..d9239c79ad 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -8,6 +8,7 @@ //= require angular-backstretch.js //= require lodash.underscore.js //= require angular-flash.min.js +//= require shared/mm-foundation-tpls-0.2.2.min.js //= require moment angular.module('templates', []) diff --git a/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee index 1d56a79d27..73ecd611ef 100644 --- a/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee @@ -3,6 +3,7 @@ describe 'ProductsCtrl', -> scope = null event = null Products = null + Cart = {} beforeEach -> module('Darkswarm') @@ -15,7 +16,7 @@ describe 'ProductsCtrl', -> inject ($controller) -> scope = {} - ctrl = $controller 'ProductsCtrl', {$scope: scope, Products: Products, OrderCycle: OrderCycle} + ctrl = $controller 'ProductsCtrl', {$scope: scope, Products: Products, OrderCycle: OrderCycle, Cart: Cart} it 'fetches products from Products', -> expect(scope.Products.products).toEqual ['testy mctest'] diff --git a/spec/javascripts/unit/darkswarm/filters/filter_groups_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/filter_groups_spec.js.coffee index e7e2614f7f..0e85fe27d3 100644 --- a/spec/javascripts/unit/darkswarm/filters/filter_groups_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/filters/filter_groups_spec.js.coffee @@ -2,7 +2,7 @@ describe "filtering Groups", -> filterGroups = null groups = [{ name: "test" - long_description: "roger" + description: "roger" enterprises: [{ name: "kittens" }, { @@ -10,7 +10,7 @@ describe "filtering Groups", -> }] }, { name: "blankness" - long_description: "in the sky" + description: "in the sky" enterprises: [{ name: "ponies" }, { diff --git a/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee index f34df5cc71..ae0d6a7c8a 100644 --- a/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee @@ -6,11 +6,8 @@ describe 'filtering urls', -> inject ($filter) -> filter = $filter('stripUrl') - it "removes http and www", -> - expect(filter("http://www.footle.com")).toEqual "footle.com" + it "removes http", -> + expect(filter("http://footle.com")).toEqual "footle.com" - it "removes https and www", -> - expect(filter("https://www.footle.com")).toEqual "footle.com" - - it "removes just www", -> - expect(filter("www.footle.com")).toEqual "footle.com" + it "removes https", -> + expect(filter("https://www.footle.com")).toEqual "www.footle.com" diff --git a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee index af384adcb6..237cbcd369 100644 --- a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee @@ -55,6 +55,12 @@ describe 'Products service', -> $httpBackend.flush() expect(Cart.line_items[0].variant).toBe Products.products[0].variants[0] + it "sets primaryImageOrMissing when no images are provided", -> + $httpBackend.expectGET("/shop/products").respond([product]) + $httpBackend.flush() + expect(Products.products[0].primaryImage).toBeUndefined() + expect(Products.products[0].primaryImageOrMissing).toEqual "/assets/noimage/small.png" + describe "determining the price to display for a product", -> it "displays the product price when the product does not have variants", -> $httpBackend.expectGET("/shop/products").respond([product]) @@ -66,4 +72,3 @@ describe 'Products service', -> $httpBackend.expectGET("/shop/products").respond([product]) $httpBackend.flush() expect(Products.products[0].price).toEqual 22 - From 77b279ca976491e1e04368f208802a83b002618f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 30 Jul 2014 14:34:05 +1000 Subject: [PATCH 009/205] Stub base_price and fees on variant serializer. Calculate basePricePercentage on variant. --- .../darkswarm/services/variants.js.coffee | 1 + app/serializers/api/variant_serializer.rb | 13 ++++++++++++- .../unit/darkswarm/services/variants_spec.js.coffee | 4 ++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/services/variants.js.coffee b/app/assets/javascripts/darkswarm/services/variants.js.coffee index d313458b75..e95a870aa8 100644 --- a/app/assets/javascripts/darkswarm/services/variants.js.coffee +++ b/app/assets/javascripts/darkswarm/services/variants.js.coffee @@ -7,4 +7,5 @@ Darkswarm.factory 'Variants', -> extend: (variant)-> variant.getPrice = -> variant.price * variant.line_item.quantity + variant.basePricePercentage = variant.base_price / variant.price * 100 variant diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index c5dbe05634..e9234285bb 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -1,8 +1,19 @@ class Api::VariantSerializer < ActiveModel::Serializer attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, - :on_demand, :price + :on_demand, :price, :fees def price object.price_with_fees(options[:current_distributor], options[:current_order_cycle]) end + + def base_price + 1.00 + end + + def fees + {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12} + end end + + +# price_without_fees / price diff --git a/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee index 278e0a6850..98dd57b5a7 100644 --- a/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee @@ -5,6 +5,8 @@ describe 'Variants service', -> beforeEach -> variant = id: 1 + base_price: 80 + price: 100 module 'Darkswarm' inject ($injector)-> Variants = $injector.get("Variants") @@ -19,3 +21,5 @@ describe 'Variants service', -> it "will return the same object as passed", -> expect(Variants.register(variant)).toBe variant + it "initialises base price percentage", -> + expect(Variants.register(variant).basePricePercentage).toEqual 80 From fc95e088dfe90e7c897f1e7838cfbd42b99ed6ab Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 30 Jul 2014 14:59:16 +1000 Subject: [PATCH 010/205] Show collapsed price breakdown --- .../darkswarm/directives/price_percentage.js.coffee | 10 ++++++++++ .../javascripts/darkswarm/services/variants.js.coffee | 2 +- .../javascripts/templates/price_breakdown.html.haml | 2 +- .../javascripts/templates/price_percentage.html.haml | 4 ++++ app/serializers/api/variant_serializer.rb | 2 +- .../unit/darkswarm/services/variants_spec.js.coffee | 4 ++-- 6 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/directives/price_percentage.js.coffee create mode 100644 app/assets/javascripts/templates/price_percentage.html.haml diff --git a/app/assets/javascripts/darkswarm/directives/price_percentage.js.coffee b/app/assets/javascripts/darkswarm/directives/price_percentage.js.coffee new file mode 100644 index 0000000000..35140598c4 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/price_percentage.js.coffee @@ -0,0 +1,10 @@ +Darkswarm.directive "pricePercentage", -> + restrict: 'E' + replace: true + templateUrl: 'price_percentage.html' + scope: + percentage: '=' + + link: (scope, elem, attrs) -> + elem.find(".meter").css + width: "#{scope.percentage}%" diff --git a/app/assets/javascripts/darkswarm/services/variants.js.coffee b/app/assets/javascripts/darkswarm/services/variants.js.coffee index e95a870aa8..0f231ac030 100644 --- a/app/assets/javascripts/darkswarm/services/variants.js.coffee +++ b/app/assets/javascripts/darkswarm/services/variants.js.coffee @@ -7,5 +7,5 @@ Darkswarm.factory 'Variants', -> extend: (variant)-> variant.getPrice = -> variant.price * variant.line_item.quantity - variant.basePricePercentage = variant.base_price / variant.price * 100 + variant.basePricePercentage = Math.round(variant.base_price / variant.price * 100) variant diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index 5e7c82e0cf..33554b93fe 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -1,4 +1,4 @@ .joyride-tip-guide{"ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} %span.joyride-nub.bottom .joyride-content-wrapper - {{ variant.id }} + %price-percentage{percentage: 'variant.basePricePercentage'} diff --git a/app/assets/javascripts/templates/price_percentage.html.haml b/app/assets/javascripts/templates/price_percentage.html.haml new file mode 100644 index 0000000000..ee1e8964a5 --- /dev/null +++ b/app/assets/javascripts/templates/price_percentage.html.haml @@ -0,0 +1,4 @@ +.progress + .meter + Cost + Fees diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index e9234285bb..74bc19e65c 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -1,6 +1,6 @@ class Api::VariantSerializer < ActiveModel::Serializer attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, - :on_demand, :price, :fees + :on_demand, :price, :fees, :base_price def price object.price_with_fees(options[:current_distributor], options[:current_order_cycle]) diff --git a/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee index 98dd57b5a7..ac9865a142 100644 --- a/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee @@ -5,7 +5,7 @@ describe 'Variants service', -> beforeEach -> variant = id: 1 - base_price: 80 + base_price: 80.5 price: 100 module 'Darkswarm' inject ($injector)-> @@ -22,4 +22,4 @@ describe 'Variants service', -> expect(Variants.register(variant)).toBe variant it "initialises base price percentage", -> - expect(Variants.register(variant).basePricePercentage).toEqual 80 + expect(Variants.register(variant).basePricePercentage).toEqual 81 From 9fa0413e8e18187a9d4ff56e6a59774fd2a32f75 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 30 Jul 2014 15:11:59 +1000 Subject: [PATCH 011/205] Add full price breakdown --- .../directives/price_breakdown.js.coffee | 5 +++- .../templates/price_breakdown.html.haml | 25 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee b/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee index 4875cf81ec..0d1d6ce0b5 100644 --- a/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee @@ -8,4 +8,7 @@ Darkswarm.directive 'priceBreakdownPopup', -> restrict: 'EA' replace: true templateUrl: 'price_breakdown.html' - scope: true + scope: false + + link: (scope, elem, attrs) -> + scope.expanded = false unless scope.expanded? diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index 33554b93fe..2508dc33da 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -1,4 +1,27 @@ .joyride-tip-guide{"ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} %span.joyride-nub.bottom .joyride-content-wrapper - %price-percentage{percentage: 'variant.basePricePercentage'} + + .collapsed{"ng-show" => "!expanded"} + %price-percentage{percentage: 'variant.basePricePercentage'} + %a{"ng-click" => "expanded = !expanded"} Full price breakdown + + .expanded{"ng-show" => "expanded"} + %ul + %li + Cost + %span {{ variant.base_price | currency }} + %li + Admin fee + %span {{ variant.fees.admin | currency }} + %li + Sales fee + %span {{ variant.fees.sales | currency }} + %li + Packing fee + %span {{ variant.fees.packing | currency }} + %li + Transport fee + %span {{ variant.fees.transport | currency }} + %a{"ng-click" => "expanded = !expanded"} Price graph + \= {{ variant.price | currency }} From 44bb53b1378a45550f81fb2b9a1a8fa823387490 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 11:11:34 +1000 Subject: [PATCH 012/205] Moving to state_name --- app/serializers/api/address_serializer.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/serializers/api/address_serializer.rb b/app/serializers/api/address_serializer.rb index 18483da312..66e4267e1f 100644 --- a/app/serializers/api/address_serializer.rb +++ b/app/serializers/api/address_serializer.rb @@ -1,10 +1,12 @@ class Api::AddressSerializer < ActiveModel::Serializer - cached - delegate :cache_key, to: :object + #cached + #delegate :cache_key, to: :object - attributes :id, :zipcode, :city, :state, :state_id + attributes :id, :zipcode, :city, :state_name, :state_id, + :phone, :firstname, :lastname, :address1, :address2, :city, :country_id, + :zipcode - def state + def state_name object.state.andand.abbr end end From 46146c83a8dcb0c72e47486eb06c4f65bcd59307 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 11:29:02 +1000 Subject: [PATCH 013/205] Make the link for product modal just go around the text and not full block --- .../stylesheets/darkswarm/_shop-product-rows.css.sass | 6 ++++-- app/views/shop/products/_summary.html.haml | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index 2dbbf46a45..fbe5949e74 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -100,8 +100,10 @@ h3 font-size: 1.5rem margin: 0 - a h3 - color: black + h3 a + color: #222 + &:hover, &:focus, &:active + color: black diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml index 77f43702cc..25c71f276a 100644 --- a/app/views/shop/products/_summary.html.haml +++ b/app/views/shop/products/_summary.html.haml @@ -4,8 +4,9 @@ .row.summary .small-9.medium-10.large-11.columns.summary-header - %a{"ng-click" => "triggerProductModal()"} - %h3 {{ product.name }} + %h3 + %a{"ng-click" => "triggerProductModal()"} + {{ product.name }} %em from %span From 659462327c71862012eaaf4ce0c356539b8c3360 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 11:29:14 +1000 Subject: [PATCH 014/205] Commenting out taxon icon for now --- app/assets/javascripts/templates/product_modal.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index 8b35cf79c5..31f51c6655 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -3,7 +3,7 @@ %img.product-img{"ng-src" => "{{product.primaryImage}}", "ng-if" => "product.primaryImage"} .columns.small-12.large-6.product-header %h2 - %render-svg{path: "{{product.primary_taxon.icon}}"} + / %render-svg{path: "{{product.primary_taxon.icon}}"} {{product.name}} %p {{product.description}} %ng-include{src: "'partials/close.html'"} From e8045cac42519bfae68b6dc78ebff21908465a11 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Wed, 30 Jul 2014 17:09:31 +1000 Subject: [PATCH 015/205] Renaming to state_name --- app/serializers/api/address_serializer.rb | 2 +- app/views/home/_fat.html.haml | 17 ----------------- app/views/home/_skinny.html.haml | 2 +- app/views/json/partials/_address.rabl | 2 +- app/views/modals/_producer.html.haml | 2 +- app/views/producers/_fat.html.haml | 2 +- app/views/producers/_skinny.html.haml | 4 ++-- 7 files changed, 7 insertions(+), 24 deletions(-) diff --git a/app/serializers/api/address_serializer.rb b/app/serializers/api/address_serializer.rb index e121cfde38..18483da312 100644 --- a/app/serializers/api/address_serializer.rb +++ b/app/serializers/api/address_serializer.rb @@ -2,7 +2,7 @@ class Api::AddressSerializer < ActiveModel::Serializer cached delegate :cache_key, to: :object - attributes :id, :zipcode, :city, :state + attributes :id, :zipcode, :city, :state, :state_id def state object.state.andand.abbr diff --git a/app/views/home/_fat.html.haml b/app/views/home/_fat.html.haml index 37b4194e26..f3f74cb282 100644 --- a/app/views/home/_fat.html.haml +++ b/app/views/home/_fat.html.haml @@ -29,20 +29,3 @@ {{ enterprise.name }} %div{"bo-if" => "!hub.producers"}   - -/ .row.active_table_row.link{"ng-show" => "open()"} -/ .cta-container.columns.small-12 -/ .row -/ .columns.small-12 -/ %h5 -/ %span{"active-table-hub-link" => "hub", change: "Change hub to", shop: "Shop at"} -/ .row -/ .columns.small-12 -/ %a.button.hub{"bo-href" => "hub.path", -/ "ng-class" => "{primary: hub.active, secondary: !hub.active}", -/ "ofn-empties-cart" => "hub"} -/ %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} -/ %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} -/ {{hub.name}} -/ .button-address {{ [hub.address.city, hub.address.state] | printArray }} -/ %i.ofn-i_007-caret-right diff --git a/app/views/home/_skinny.html.haml b/app/views/home/_skinny.html.haml index dad391a287..2b59132999 100644 --- a/app/views/home/_skinny.html.haml +++ b/app/views/home/_skinny.html.haml @@ -7,7 +7,7 @@ .columns.small-4.medium-2.large-2 {{ hub.address.city }} .columns.small-2.medium-1.large-1 - {{ hub.address.state | uppercase }} + {{ hub.address.state_name | uppercase }} .columns.small-6.medium-3.large-4.text-right{"bo-if" => "hub.active"} %a.hub{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"} diff --git a/app/views/json/partials/_address.rabl b/app/views/json/partials/_address.rabl index 04cc456a29..8f77f83d99 100644 --- a/app/views/json/partials/_address.rabl +++ b/app/views/json/partials/_address.rabl @@ -1,4 +1,4 @@ attributes :city, :zipcode, :phone -node :state do |address| +node :state_name do |address| address.state.abbr end diff --git a/app/views/modals/_producer.html.haml b/app/views/modals/_producer.html.haml index 8fd778b388..5d2d342f6d 100644 --- a/app/views/modals/_producer.html.haml +++ b/app/views/modals/_producer.html.haml @@ -2,7 +2,7 @@ .highlight .highlight-top %p.right - {{ [enterprise.address.city, enterprise.address.state] | printArray}} + {{ [enterprise.address.city, enterprise.address.state_name] | printArray}} %h3 %i.ofn-i_036-producers {{ enterprise.name }} diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml index 10cd2f00b5..042e39dba9 100644 --- a/app/views/producers/_fat.html.haml +++ b/app/views/producers/_fat.html.haml @@ -34,5 +34,5 @@ %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} {{hub.name}} - .button-address {{ [hub.address.city, hub.address.state] | printArray }} + .button-address {{ [hub.address.city, hub.address.state_name] | printArray }} %i.ofn-i_007-caret-right diff --git a/app/views/producers/_skinny.html.haml b/app/views/producers/_skinny.html.haml index 824d8f2630..e1f26f0304 100644 --- a/app/views/producers/_skinny.html.haml +++ b/app/views/producers/_skinny.html.haml @@ -5,8 +5,8 @@ .columns.small-6.medium-3.large-3 {{ producer.address.city }} .columns.small-4.medium-3.large-4 - {{ producer.address.state | uppercase }} + {{ producer.address.state_name | uppercase }} .columns.small-2.medium-2.large-1.text-right / This forces line-height to be triggered %span   - %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} \ No newline at end of file + %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} From 66cece5903d0ca92dd77e9e0e7f8e39a9a3dbacb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 30 Jul 2014 17:46:21 +1000 Subject: [PATCH 016/205] WIP: Extract order cycle fee calculations to EnterpriseFeeCalculator --- app/models/order_cycle.rb | 78 +--------- .../enterprise_fee_calculator.rb | 75 ++++++++++ .../enterprise_fee_applicator_spec.rb | 1 - .../enterprise_fee_calculator_spec.rb | 138 ++++++++++++++++++ spec/models/order_cycle_spec.rb | 101 ------------- 5 files changed, 217 insertions(+), 176 deletions(-) create mode 100644 lib/open_food_network/enterprise_fee_calculator.rb create mode 100644 spec/lib/open_food_network/enterprise_fee_calculator_spec.rb diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index f2ad8fee3e..d5a1371876 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -1,5 +1,3 @@ -require 'open_food_network/enterprise_fee_applicator' - class OrderCycle < ActiveRecord::Base belongs_to :coordinator, :class_name => 'Enterprise' has_and_belongs_to_many :coordinator_fees, :class_name => 'EnterpriseFee', :join_table => 'coordinator_fees' @@ -165,77 +163,17 @@ class OrderCycle < ActiveRecord::Base exchange_for_distributor(distributor).andand.pickup_instructions end - - # -- Fees - - # TODO: The boundary of this class is ill-defined here. OrderCycle should not know about - # EnterpriseFeeApplicator. Clients should be able to query it for relevant EnterpriseFees. - # This logic would fit better in another service object. - - def fees_for(variant, distributor) - per_item_enterprise_fee_applicators_for(variant, distributor).sum do |applicator| - # Spree's Calculator interface accepts Orders or LineItems, - # so we meet that interface with a struct. - # Amount is faked, this is a method on LineItem - line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price - applicator.enterprise_fee.compute_amount(line_item) - end + def exchanges_carrying(variant, distributor) + exchanges.supplying_to(distributor).with_variant(variant) end - def create_line_item_adjustments_for(line_item) - variant = line_item.variant - distributor = line_item.order.distributor - - per_item_enterprise_fee_applicators_for(variant, distributor).each do |applicator| - applicator.create_line_item_adjustment(line_item) - end - end - - def create_order_adjustments_for(order) - per_order_enterprise_fee_applicators_for(order).each do |applicator| - applicator.create_order_adjustment(order) - end + def exchanges_supplying(order) + exchanges.supplying_to(order.distributor).with_any_variant(order.variants) end private - # -- Fees - def per_item_enterprise_fee_applicators_for(variant, distributor) - fees = [] - - exchanges_carrying(variant, distributor).each do |exchange| - exchange.enterprise_fees.per_item.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) - end - end - - coordinator_fees.per_item.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator') - end - - fees - end - - def per_order_enterprise_fee_applicators_for(order) - fees = [] - - exchanges_supplying(order).each do |exchange| - exchange.enterprise_fees.per_order.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) - end - end - - coordinator_fees.per_order.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator') - end - - fees - end - - - # -- Misc - # If a product without variants is added to an order cycle, and then some variants are added # to that product, then the master variant is still part of the order cycle, but customers # should not be able to purchase it. @@ -246,12 +184,4 @@ class OrderCycle < ActiveRecord::Base distributed_variants.include?(product.master) && (product.variants & distributed_variants).empty? end - - def exchanges_carrying(variant, distributor) - exchanges.supplying_to(distributor).with_variant(variant) - end - - def exchanges_supplying(order) - exchanges.supplying_to(order.distributor).with_any_variant(order.variants) - end end diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb new file mode 100644 index 0000000000..bbd220cfad --- /dev/null +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -0,0 +1,75 @@ +require 'open_food_network/enterprise_fee_applicator' + +module OpenFoodNetwork + class EnterpriseFeeCalculator + def initialize(distributor=nil, order_cycle=nil) + @distributor = distributor + @order_cycle = order_cycle + end + + + def fees_for(variant) + per_item_enterprise_fee_applicators_for(variant).sum do |applicator| + # Spree's Calculator interface accepts Orders or LineItems, + # so we meet that interface with a struct. + # Amount is faked, this is a method on LineItem + line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price + applicator.enterprise_fee.compute_amount(line_item) + end + end + + def create_line_item_adjustments_for(line_item) + variant = line_item.variant + @distributor = line_item.order.distributor + @order_cycle = line_item.order.order_cycle + + per_item_enterprise_fee_applicators_for(variant).each do |applicator| + applicator.create_line_item_adjustment(line_item) + end + end + + def create_order_adjustments_for(order) + @distributor = order.distributor + @order_cycle = order.order_cycle + + per_order_enterprise_fee_applicators_for(order).each do |applicator| + applicator.create_order_adjustment(order) + end + end + + + private + + def per_item_enterprise_fee_applicators_for(variant) + fees = [] + + @order_cycle.exchanges_carrying(variant, @distributor).each do |exchange| + exchange.enterprise_fees.per_item.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) + end + end + + @order_cycle.coordinator_fees.per_item.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator') + end + + fees + end + + def per_order_enterprise_fee_applicators_for(order) + fees = [] + + @order_cycle.exchanges_supplying(order).each do |exchange| + exchange.enterprise_fees.per_order.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) + end + end + + @order_cycle.coordinator_fees.per_order.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator') + end + + fees + end + end +end diff --git a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb index d10a5e9b72..9ad1d222b3 100644 --- a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb @@ -27,7 +27,6 @@ module OpenFoodNetwork it "creates an adjustment for an order" do order = create(:order) - #line_item = create(:line_item) enterprise_fee = create(:enterprise_fee) product = create(:simple_product) diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb new file mode 100644 index 0000000000..697bb3c702 --- /dev/null +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -0,0 +1,138 @@ +require 'open_food_network/enterprise_fee_calculator' + +module OpenFoodNetwork + describe EnterpriseFeeCalculator do + describe "integration" do + let(:coordinator) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle) } + let(:product) { create(:simple_product, price: 10.00) } + + describe "calculating fees for a variant via a particular distribution" do + it "sums all the per-item fees for the variant in the specified hub + order cycle" do + enterprise_fee1 = create(:enterprise_fee, amount: 20) + enterprise_fee2 = create(:enterprise_fee, amount: 3) + enterprise_fee3 = create(:enterprise_fee, + calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) + + create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [enterprise_fee1, enterprise_fee2, enterprise_fee3], variants: [product.master]) + + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product.master).should == 23 + end + + it "sums percentage fees for the variant" do + enterprise_fee1 = create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) + + create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [enterprise_fee1], variants: [product.master]) + + product.master.price.should == 10.00 + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product.master).should == 2.00 + end + end + + describe "creating adjustments" do + let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) } + let!(:line_item) { create(:line_item, order: order, variant: product.master) } + let(:enterprise_fee_line_item) { create(:enterprise_fee) } + let(:enterprise_fee_order) { create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) } + let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, variants: [product.master]) } + + before { order.reload } + + it "creates adjustments for a line item" do + exchange.enterprise_fees << enterprise_fee_line_item + + EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item + + a = Spree::Adjustment.last + a.metadata.fee_name.should == enterprise_fee_line_item.name + end + + it "creates adjustments for an order" do + exchange.enterprise_fees << enterprise_fee_order + + EnterpriseFeeCalculator.new.create_order_adjustments_for order + + a = Spree::Adjustment.last + a.metadata.fee_name.should == enterprise_fee_order.name + end + end + end + + describe "creating adjustments for a line item" do + let(:oc) { OrderCycle.new } + let(:variant) { double(:variant) } + let(:distributor) { double(:distributor) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + let(:line_item) { double(:line_item, variant: variant, order: order) } + + it "creates an adjustment for each fee" do + applicator = double(:enterprise_fee_applicator) + applicator.should_receive(:create_line_item_adjustment).with(line_item) + + efc = EnterpriseFeeCalculator.new + efc.should_receive(:per_item_enterprise_fee_applicators_for).with(variant) { [applicator] } + + efc.create_line_item_adjustments_for line_item + end + + it "makes fee applicators for a line item" do + distributor = double(:distributor) + ef1 = double(:enterprise_fee) + ef2 = double(:enterprise_fee) + ef3 = double(:enterprise_fee) + incoming_exchange = double(:exchange, role: 'supplier') + outgoing_exchange = double(:exchange, role: 'distributor') + incoming_exchange.stub_chain(:enterprise_fees, :per_item) { [ef1] } + outgoing_exchange.stub_chain(:enterprise_fees, :per_item) { [ef2] } + + oc.stub(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } + oc.stub_chain(:coordinator_fees, :per_item) { [ef3] } + + efc = EnterpriseFeeCalculator.new(distributor, oc) + efc.send(:per_item_enterprise_fee_applicators_for, line_item.variant).should == + [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] + end + end + + describe "creating adjustments for an order" do + let(:oc) { OrderCycle.new } + let(:distributor) { double(:distributor) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + + it "creates an adjustment for each fee" do + applicator = double(:enterprise_fee_applicator) + applicator.should_receive(:create_order_adjustment).with(order) + + efc = EnterpriseFeeCalculator.new + efc.should_receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } + + efc.create_order_adjustments_for order + end + + it "makes fee applicators for an order" do + distributor = double(:distributor) + ef1 = double(:enterprise_fee) + ef2 = double(:enterprise_fee) + ef3 = double(:enterprise_fee) + incoming_exchange = double(:exchange, role: 'supplier') + outgoing_exchange = double(:exchange, role: 'distributor') + incoming_exchange.stub_chain(:enterprise_fees, :per_order) { [ef1] } + outgoing_exchange.stub_chain(:enterprise_fees, :per_order) { [ef2] } + + oc.stub(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } + oc.stub_chain(:coordinator_fees, :per_order) { [ef3] } + + efc = EnterpriseFeeCalculator.new(distributor, oc) + efc.send(:per_order_enterprise_fee_applicators_for, order).should == + [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] + end + end + end +end diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index e4f979dc5b..9785d5ed2d 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -368,107 +368,6 @@ describe OrderCycle do end end - describe "calculating fees for a variant via a particular distributor" do - it "sums all the per-item fees for the variant in the specified hub + order cycle" do - coordinator = create(:distributor_enterprise) - distributor = create(:distributor_enterprise) - order_cycle = create(:simple_order_cycle) - enterprise_fee1 = create(:enterprise_fee, amount: 20) - enterprise_fee2 = create(:enterprise_fee, amount: 3) - enterprise_fee3 = create(:enterprise_fee, - calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) - product = create(:simple_product) - - create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, - enterprise_fees: [enterprise_fee1, enterprise_fee2, enterprise_fee3], variants: [product.master]) - - order_cycle.fees_for(product.master, distributor).should == 23 - end - - - it "sums percentage fees for the variant" do - coordinator = create(:distributor_enterprise) - distributor = create(:distributor_enterprise) - order_cycle = create(:simple_order_cycle) - enterprise_fee1 = create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) - product = create(:simple_product, price: 10.00) - - create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, - enterprise_fees: [enterprise_fee1], variants: [product.master]) - - product.master.price.should == 10.00 - order_cycle.fees_for(product.master, distributor).should == 2.00 - end - end - - describe "creating adjustments for a line item" do - let(:oc) { OrderCycle.new } - let(:variant) { double(:variant) } - let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor) } - let(:line_item) { double(:line_item, variant: variant, order: order) } - - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_line_item_adjustment).with(line_item) - oc.should_receive(:per_item_enterprise_fee_applicators_for).with(variant, distributor) { [applicator] } - - oc.send(:create_line_item_adjustments_for, line_item) - end - - it "makes fee applicators for a line item" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_item) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_item) { [ef2] } - - oc.stub(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_item) { [ef3] } - - oc.send(:per_item_enterprise_fee_applicators_for, line_item.variant, distributor).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] - end - end - - describe "creating adjustments for an order" do - let(:oc) { OrderCycle.new } - let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor) } - - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_order_adjustment).with(order) - oc.should_receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } - - oc.send(:create_order_adjustments_for, order) - end - - it "makes fee applicators for an order" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_order) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_order) { [ef2] } - - oc.stub(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_order) { [ef3] } - - oc.send(:per_order_enterprise_fee_applicators_for, order).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] - end - end - describe "finding recently closed order cycles" do it "should give the most recently closed order cycle for a distributor" do distributor = create(:distributor_enterprise) From b40b6f9faf916959d5784a438a8aa85442d348f0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 31 Jul 2014 09:19:57 +1000 Subject: [PATCH 017/205] WIP: Call fees_for on EnterpriseFeeCalculator --- app/models/spree/variant_decorator.rb | 3 ++- spec/models/spree/variant_spec.rb | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 63ae5e0f3a..1ad2dd75d0 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -1,3 +1,4 @@ +require 'open_food_network/enterprise_fee_calculator' require 'open_food_network/option_value_namer' Spree::Variant.class_eval do @@ -44,7 +45,7 @@ Spree::Variant.class_eval do end def fees_for(distributor, order_cycle) - order_cycle.fees_for(self, distributor) + OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for self end diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 5ddff963df..d1d833664a 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -82,12 +82,13 @@ module Spree describe "calculating the fees" do - it "delegates to order cycle" do + it "delegates to EnterpriseFeeCalculator" do distributor = double(:distributor) order_cycle = double(:order_cycle) variant = Variant.new - order_cycle.should_receive(:fees_for).with(variant, distributor) { 23 } + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.should_receive(:fees_for).with(variant) { 23 } + variant.fees_for(distributor, order_cycle).should == 23 end end From 8fe355b6da110d4b7c81b09ab70595b8e7d9c4bb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 31 Jul 2014 09:27:36 +1000 Subject: [PATCH 018/205] WIP: Call create_line_item_adjustments_for on EnterpriseFeeCalculator --- app/models/spree/order_decorator.rb | 5 +++-- spec/models/spree/order_spec.rb | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 4f2be99fa8..7cd5967f6e 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -1,5 +1,6 @@ -require 'open_food_network/feature_toggle' +require 'open_food_network/enterprise_fee_calculator' require 'open_food_network/distribution_change_validator' +require 'open_food_network/feature_toggle' ActiveSupport::Notifications.subscribe('spree.order.contents_changed') do |name, start, finish, id, payload| payload[:order].reload.update_distribution_charge! @@ -133,7 +134,7 @@ Spree::Order.class_eval do line_items.each do |line_item| if provided_by_order_cycle? line_item - order_cycle.create_line_item_adjustments_for line_item + OpenFoodNetwork::EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item else pd = product_distribution_for line_item diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 898a08d075..e477e68e0e 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -82,7 +82,9 @@ describe Spree::Order do subject.stub(:provided_by_order_cycle?) { true } order_cycle = double(:order_cycle) - order_cycle.should_receive(:create_line_item_adjustments_for).with(line_item) + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance. + should_receive(:create_line_item_adjustments_for). + with(line_item) order_cycle.stub(:create_order_adjustments_for) subject.stub(:order_cycle) { order_cycle } From 45fd479ade3da5eaaa82074a2660ae3a4046072a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 31 Jul 2014 09:35:21 +1000 Subject: [PATCH 019/205] WIP: Call create_order_adjustments_for on EnterpriseFeeCalculator --- app/models/spree/order_decorator.rb | 4 +++- spec/models/spree/order_spec.rb | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 7cd5967f6e..fcc6054f3b 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -142,7 +142,9 @@ Spree::Order.class_eval do end end - order_cycle.create_order_adjustments_for self if order_cycle + if order_cycle + OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self + end end def set_variant_attributes(variant, attributes) diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index e477e68e0e..085ee615c6 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -85,7 +85,7 @@ describe Spree::Order do OpenFoodNetwork::EnterpriseFeeCalculator.any_instance. should_receive(:create_line_item_adjustments_for). with(line_item) - order_cycle.stub(:create_order_adjustments_for) + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.stub(:create_order_adjustments_for) subject.stub(:order_cycle) { order_cycle } subject.update_distribution_charge! @@ -96,7 +96,10 @@ describe Spree::Order do subject.stub(:line_items) { [] } order_cycle = double(:order_cycle) - order_cycle.should_receive(:create_order_adjustments_for).with(subject) + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance. + should_receive(:create_order_adjustments_for). + with(subject) + subject.stub(:order_cycle) { order_cycle } subject.update_distribution_charge! From 8c41a6c9097cd3d4476d83afe914bdbda801f024 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 31 Jul 2014 10:09:46 +1000 Subject: [PATCH 020/205] Calculate fee breakdown --- app/models/spree/variant_decorator.rb | 4 ++++ app/serializers/api/variant_serializer.rb | 7 ++---- .../enterprise_fee_calculator.rb | 23 +++++++++++++++---- .../enterprise_fee_calculator_spec.rb | 17 +++++++++++++- spec/models/spree/variant_spec.rb | 14 +++++++++++ 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 1ad2dd75d0..7d4f178488 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -48,6 +48,10 @@ Spree::Variant.class_eval do OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for self end + def fees_by_type_for(distributor, order_cycle) + OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for self + end + # Copied and modified from Spree::Variant def options_text diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index 74bc19e65c..e03f1e0ec8 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -7,13 +7,10 @@ class Api::VariantSerializer < ActiveModel::Serializer end def base_price - 1.00 + object.price end def fees - {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12} + object.fees_by_type_for(options[:current_distributor], options[:current_order_cycle]) end end - - -# price_without_fees / price diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb index bbd220cfad..2d76d8d280 100644 --- a/lib/open_food_network/enterprise_fee_calculator.rb +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -10,14 +10,19 @@ module OpenFoodNetwork def fees_for(variant) per_item_enterprise_fee_applicators_for(variant).sum do |applicator| - # Spree's Calculator interface accepts Orders or LineItems, - # so we meet that interface with a struct. - # Amount is faked, this is a method on LineItem - line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price - applicator.enterprise_fee.compute_amount(line_item) + calculate_fee_for variant, applicator end end + def fees_by_type_for(variant) + per_item_enterprise_fee_applicators_for(variant).inject({}) do |fees, applicator| + fees[applicator.enterprise_fee.fee_type.to_sym] ||= 0 + fees[applicator.enterprise_fee.fee_type.to_sym] += calculate_fee_for variant, applicator + fees + end + end + + def create_line_item_adjustments_for(line_item) variant = line_item.variant @distributor = line_item.order.distributor @@ -40,6 +45,14 @@ module OpenFoodNetwork private + def calculate_fee_for(variant, applicator) + # Spree's Calculator interface accepts Orders or LineItems, + # so we meet that interface with a struct. + # Amount is faked, this is a method on LineItem + line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price + applicator.enterprise_fee.compute_amount(line_item) + end + def per_item_enterprise_fee_applicators_for(variant) fees = [] diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb index 697bb3c702..21c4848fda 100644 --- a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -8,7 +8,7 @@ module OpenFoodNetwork let(:order_cycle) { create(:simple_order_cycle) } let(:product) { create(:simple_product, price: 10.00) } - describe "calculating fees for a variant via a particular distribution" do + describe "calculating fees for a variant" do it "sums all the per-item fees for the variant in the specified hub + order cycle" do enterprise_fee1 = create(:enterprise_fee, amount: 20) enterprise_fee2 = create(:enterprise_fee, amount: 3) @@ -32,6 +32,21 @@ module OpenFoodNetwork end end + describe "calculating fees by type" do + let!(:ef_admin) { create(:enterprise_fee, fee_type: 'admin', amount: 1.23) } + let!(:ef_sales) { create(:enterprise_fee, fee_type: 'sales', amount: 4.56) } + let!(:ef_packing) { create(:enterprise_fee, fee_type: 'packing', amount: 7.89) } + let!(:ef_transport) { create(:enterprise_fee, fee_type: 'transport', amount: 0.12) } + let!(:exchange) { create(:exchange, order_cycle: order_cycle, + sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [ef_admin, ef_sales, ef_packing, ef_transport], + variants: [product.master]) } + + it "returns a breakdown of fees" do + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12} + end + end + describe "creating adjustments" do let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) } let!(:line_item) { create(:line_item, order: order, variant: product.master) } diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index d1d833664a..7fb160ce3f 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -94,6 +94,20 @@ module Spree end + describe "calculating fees broken down by fee type" do + it "delegates to EnterpriseFeeCalculator" do + distributor = double(:distributor) + order_cycle = double(:order_cycle) + variant = Variant.new + fees = double(:fees) + + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.should_receive(:fees_by_type_for).with(variant) { fees } + + variant.fees_by_type_for(distributor, order_cycle).should == fees + end + end + + context "when the product has variants" do let!(:product) { create(:simple_product) } let!(:variant) { create(:variant, product: product) } From 126e0a1f6d23b47f69c9f09184103a3cc40cc221 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 31 Jul 2014 11:34:21 +1000 Subject: [PATCH 021/205] Link variants -> products, fixes group buy --- app/assets/javascripts/darkswarm/services/products.js.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/darkswarm/services/products.js.coffee b/app/assets/javascripts/darkswarm/services/products.js.coffee index aec7b1cc3e..5819c7b661 100644 --- a/app/assets/javascripts/darkswarm/services/products.js.coffee +++ b/app/assets/javascripts/darkswarm/services/products.js.coffee @@ -28,6 +28,8 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Car for product in @products 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 registerVariantsWithCart: -> From a1dd5dabac1d0da593e1dd4dade344a9af6f769a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 31 Jul 2014 11:36:34 +1000 Subject: [PATCH 022/205] Replace sleep with wait, add cart_dirty spec helper --- app/views/shared/menu/_cart.html.haml | 2 +- spec/features/consumer/shopping/shopping_spec.rb | 4 +++- spec/support/request/ui_component_helper.rb | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index 2d8fbebcb9..907fda0eff 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -1,4 +1,4 @@ -%span.cart-span{"ng-controller" => "CartCtrl"} +%span.cart-span{"ng-controller" => "CartCtrl", "ng-class" => "{ dirty: Cart.dirty }"} %a#cart.icon{cart: true} %span.nav-branded %i.ofn-i_027-shopping-cart diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 288e628b38..56c578f91b 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -139,7 +139,9 @@ feature "As a consumer I want to shop with a distributor", js: true do it "should save group buy data to ze cart" do fill_in "variants[#{product.master.id}]", with: 5 fill_in "variant_attributes[#{product.master.id}][max_quantity]", with: 9 - sleep 5 + + wait_until { !cart_dirty } + li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last li.max_quantity.should == 9 li.quantity.should == 5 diff --git a/spec/support/request/ui_component_helper.rb b/spec/support/request/ui_component_helper.rb index 7ca025e235..d9f01b447b 100644 --- a/spec/support/request/ui_component_helper.rb +++ b/spec/support/request/ui_component_helper.rb @@ -68,6 +68,10 @@ module UIComponentHelper find("#cart").click end + def cart_dirty + page.find("span.cart-span")[:class].include? 'dirty' + end + def wait_for_ajax counter = 0 while page.execute_script("return $.active").to_i > 0 From 29b3a080acde70bc7754a140f82216990eceffc5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 31 Jul 2014 11:43:02 +1000 Subject: [PATCH 023/205] Only show fees if present --- .../javascripts/templates/price_breakdown.html.haml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index 2508dc33da..518448f565 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -1,4 +1,4 @@ -.joyride-tip-guide{"ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} +.joyride-tip-guide{bindonce: true, "ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} %span.joyride-nub.bottom .joyride-content-wrapper @@ -11,16 +11,16 @@ %li Cost %span {{ variant.base_price | currency }} - %li + %li{"bo-if" => "variant.fees.admin"} Admin fee %span {{ variant.fees.admin | currency }} - %li + %li{"bo-if" => "variant.fees.sales"} Sales fee %span {{ variant.fees.sales | currency }} - %li + %li{"bo-if" => "variant.fees.packing"} Packing fee %span {{ variant.fees.packing | currency }} - %li + %li{"bo-if" => "variant.fees.transport"} Transport fee %span {{ variant.fees.transport | currency }} %a{"ng-click" => "expanded = !expanded"} Price graph From 86c1d010e7d2501409e22bd7c0f55ae79f91e3bd Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 11:49:34 +1000 Subject: [PATCH 024/205] Default quantity to null rather than zero --- app/assets/javascripts/darkswarm/services/cart.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index 2d91eab370..5123bcf508 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -57,6 +57,6 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> create_line_item: (variant)-> variant.line_item = variant: variant - quantity: 0 + quantity: null max_quantity: null @line_items.push variant.line_item From 33a177ed479bb1b68d8471a531caef3cc4f20eec Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 12:00:24 +1000 Subject: [PATCH 025/205] Make map input placeholder message useful --- .../javascripts/darkswarm/directives/map_search.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/directives/map_search.js.coffee b/app/assets/javascripts/darkswarm/directives/map_search.js.coffee index 4942032580..88456db2a3 100644 --- a/app/assets/javascripts/darkswarm/directives/map_search.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/map_search.js.coffee @@ -3,7 +3,7 @@ Darkswarm.directive 'mapSearch', ($timeout)-> restrict: 'E' require: '^googleMap' replace: true - template: '' + template: '' link: (scope, elem, attrs, ctrl)-> $timeout => map = ctrl.getMap() From ee067d48044fa63235bebd8c0f6dbc5b9ed1f8bf Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 12:00:36 +1000 Subject: [PATCH 026/205] Style the map input --- app/assets/stylesheets/darkswarm/map.css.sass | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/map.css.sass b/app/assets/stylesheets/darkswarm/map.css.sass index 2050cf5063..78d350d6be 100644 --- a/app/assets/stylesheets/darkswarm/map.css.sass +++ b/app/assets/stylesheets/darkswarm/map.css.sass @@ -1,6 +1,8 @@ // Place all the styles related to the map controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ +@import big-input + .map-container width: 100% map, .angular-google-map-container, google-map, .angular-google-map @@ -11,6 +13,14 @@ max-width: none height: auto - #pac-input - padding: 4px - font-size: 2em + #pac-input + @include big-input(#888, #333, $clr-brick) + @include big-input-static + font-size: 1.5rem + background: rgba(255,255,255,0.85) + width: 50% + margin-top: 1.2rem + @media all and (max-width: 768px) + width: 80% + &:active, &:focus, &.active + background: rgba(255,255,255, 1) From dc1963497afe9e6a44b26313457ec2b33ce12a84 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 12:03:26 +1000 Subject: [PATCH 027/205] SORTING BY PRIMARY TAXON NAME --- .../darkswarm/controllers/products_controller.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index da595418da..8ed56f252d 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -6,7 +6,8 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, $scope.filterText = FilterSelectorsService.filterText $scope.FilterSelectorsService = FilterSelectorsService $scope.limit = 3 - $scope.ordering = {order: "name"} + $scope.ordering = + order: "primary_taxon.name" $scope.order_cycle = OrderCycle.order_cycle $scope.incrementLimit = -> From d70ed029775896056b0797948d1f44fb0cc98916 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 12:04:12 +1000 Subject: [PATCH 028/205] Restoring caching on addresses --- app/serializers/api/address_serializer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/serializers/api/address_serializer.rb b/app/serializers/api/address_serializer.rb index 66e4267e1f..2134c7c0b0 100644 --- a/app/serializers/api/address_serializer.rb +++ b/app/serializers/api/address_serializer.rb @@ -1,6 +1,6 @@ class Api::AddressSerializer < ActiveModel::Serializer - #cached - #delegate :cache_key, to: :object + cached + delegate :cache_key, to: :object attributes :id, :zipcode, :city, :state_name, :state_id, :phone, :firstname, :lastname, :address1, :address2, :city, :country_id, From 0f76892a5b533605ef8bf0a1e9f483910a01a1ca Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 12:23:10 +1000 Subject: [PATCH 029/205] Handling max quantity magically --- app/assets/javascripts/darkswarm/directives/max.js.coffee | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 app/assets/javascripts/darkswarm/directives/max.js.coffee diff --git a/app/assets/javascripts/darkswarm/directives/max.js.coffee b/app/assets/javascripts/darkswarm/directives/max.js.coffee new file mode 100644 index 0000000000..ed3d7b3253 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/max.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.directive "max", -> + restrict: 'A' + link: (scope, elem, attr)-> + elem.bind 'input', -> + if elem.val() > +attr.max + elem.val attr.max From d9f3dfb5714aad2a8f06abc50791115f09b19862 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 12:23:22 +1000 Subject: [PATCH 030/205] Removing a pointless error message --- app/assets/javascripts/darkswarm/services/cart.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index 5123bcf508..aa6820f150 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -19,7 +19,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> $http.post('/orders/populate', @data()).success (data, status)=> @saved() .error (response, status)=> - alert "There was an error on the server! Please refresh the page" + # TODO what shall we do here? data: => variants = {} From a26450d17398c3235836482d073c9938b809a8a5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 31 Jul 2014 14:58:18 +1000 Subject: [PATCH 031/205] Remove unused spree searcher class - causing intermittent test failures --- config/initializers/spree.rb | 2 - lib/open_food_network/searcher.rb | 37 --------------- spec/lib/open_food_network/searcher_spec.rb | 52 --------------------- 3 files changed, 91 deletions(-) delete mode 100644 lib/open_food_network/searcher.rb delete mode 100644 spec/lib/open_food_network/searcher_spec.rb diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index 0460865c7e..bbee30379a 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -8,13 +8,11 @@ require 'spree/product_filters' -require 'open_food_network/searcher' Spree.config do |config| config.shipping_instructions = true config.checkout_zone = 'Australia' config.address_requires_state = true - config.searcher_class = OpenFoodNetwork::Searcher # 12 should be Australia. Hardcoded for CI (Jenkins), where countries are not pre-loaded. config.default_country_id = 12 diff --git a/lib/open_food_network/searcher.rb b/lib/open_food_network/searcher.rb deleted file mode 100644 index 83781ea475..0000000000 --- a/lib/open_food_network/searcher.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spree/core/search/base' - -module OpenFoodNetwork - class Searcher < Spree::Core::Search::Base - - # Do not perform pagination - def retrieve_products - @products_scope = get_base_scope - curr_page = page || 1 - - @products = @products_scope.includes([:master]) - end - - def get_base_scope - base_scope = super - - # The concern of separating products by distributor and order cycle is dealt with in - # a few other places: OpenFoodNetwork::SplitProductsByDistribution (for splitting the main - # product display) and Spree::BaseHelper decorator (for taxon counts). - - base_scope = base_scope.in_supplier_or_distributor(enterprise_id) if enterprise_id - base_scope = base_scope.in_supplier(supplier_id) if supplier_id - base_scope = base_scope.in_distributor(distributor_id) if distributor_id - - base_scope - end - - - def prepare(params) - super(params) - @properties[:enterprise_id] = params[:enterprise_id] - @properties[:supplier_id] = params[:supplier_id] - @properties[:distributor_id] = params[:distributor_id] - end - - end -end diff --git a/spec/lib/open_food_network/searcher_spec.rb b/spec/lib/open_food_network/searcher_spec.rb deleted file mode 100644 index 2fb83ed309..0000000000 --- a/spec/lib/open_food_network/searcher_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' -require 'open_food_network/searcher' - -module OpenFoodNetwork - describe Searcher do - it "searches by supplier" do - # Given products under two suppliers - s1 = create(:supplier_enterprise) - s2 = create(:supplier_enterprise) - p1 = create(:product, :supplier => s1) - p2 = create(:product, :supplier => s2) - - # When we search for one supplier, we should see only products from that supplier - searcher = Searcher.new(:supplier_id => s1.id.to_s) - products = searcher.retrieve_products - products.should == [p1] - end - - it "searches by distributor" do - # Given products under two distributors - d1 = create(:distributor_enterprise) - d2 = create(:distributor_enterprise) - p1 = create(:product, :distributors => [d1]) - p2 = create(:product, :distributors => [d2]) - - # When we search for one distributor, we should see only products from that distributor - searcher = Searcher.new(:distributor_id => d1.id.to_s) - products = searcher.retrieve_products - products.should == [p1] - end - - it "searches by supplier or distributor" do - # Given products under some suppliers and distributors - s0 = create(:supplier_enterprise) - s1 = create(:supplier_enterprise) - d1 = create(:distributor_enterprise) - p1 = create(:product, :supplier => s1) - p2 = create(:product, :distributors => [d1]) - p3 = create(:product, :supplier => s0) - - # When we search by the supplier enterprise, we should see the supplied products - searcher = Searcher.new(:enterprise_id => s1.id.to_s) - products = searcher.retrieve_products - products.should == [p1] - - # When we search by the distributor enterprise, we should see the distributed products - searcher = Searcher.new(:enterprise_id => d1.id.to_s) - products = searcher.retrieve_products - products.should == [p2] - end - end -end From c4984144d3b701320a584416e0d4e7cad279e4d5 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 15:05:29 +1000 Subject: [PATCH 032/205] Turn off blue highlight in Chrome for all buttons across the system --- app/assets/stylesheets/darkswarm/ui.css.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/darkswarm/ui.css.sass b/app/assets/stylesheets/darkswarm/ui.css.sass index bd740ec614..9d28e16787 100644 --- a/app/assets/stylesheets/darkswarm/ui.css.sass +++ b/app/assets/stylesheets/darkswarm/ui.css.sass @@ -46,6 +46,7 @@ .button, button @include border-radius(0.5em) + outline: none // Turn off blue highlight on chrome .button.primary, button.primary font-family: 'Open Sans', Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif From 44d7ae8b611ff1d00b91a3f9154d40659ec65e2b Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 15:07:22 +1000 Subject: [PATCH 033/205] Tweak the markup for brice breakdown and percentage for styling --- .../templates/price_breakdown.html.haml | 29 ++++++++++++------- .../templates/price_percentage.html.haml | 3 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index 518448f565..f9919b92ff 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -1,27 +1,36 @@ .joyride-tip-guide{bindonce: true, "ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} - %span.joyride-nub.bottom + %span.joyride-nub.right .joyride-content-wrapper .collapsed{"ng-show" => "!expanded"} %price-percentage{percentage: 'variant.basePricePercentage'} - %a{"ng-click" => "expanded = !expanded"} Full price breakdown + %a{"ng-click" => "expanded = !expanded"} + Full price breakdown + %i.ofn-i_005-caret-down .expanded{"ng-show" => "expanded"} %ul - %li + %li.cost + .right {{ variant.base_price | currency }} Cost - %span {{ variant.base_price | currency }} %li{"bo-if" => "variant.fees.admin"} + .right {{ variant.fees.admin | currency }} Admin fee - %span {{ variant.fees.admin | currency }} %li{"bo-if" => "variant.fees.sales"} + .right {{ variant.fees.sales | currency }} Sales fee - %span {{ variant.fees.sales | currency }} %li{"bo-if" => "variant.fees.packing"} + .right {{ variant.fees.packing | currency }} Packing fee - %span {{ variant.fees.packing | currency }} %li{"bo-if" => "variant.fees.transport"} + .right {{ variant.fees.transport | currency }} Transport fee - %span {{ variant.fees.transport | currency }} - %a{"ng-click" => "expanded = !expanded"} Price graph - \= {{ variant.price | currency }} + %li + %strong + .right = {{ variant.price | currency }} +   + + %a{"ng-click" => "expanded = !expanded"} + Price graph + %i.ofn-i_006-caret-up + diff --git a/app/assets/javascripts/templates/price_percentage.html.haml b/app/assets/javascripts/templates/price_percentage.html.haml index ee1e8964a5..4982f63eb7 100644 --- a/app/assets/javascripts/templates/price_percentage.html.haml +++ b/app/assets/javascripts/templates/price_percentage.html.haml @@ -1,4 +1,5 @@ .progress + .right Fees .meter Cost - Fees + From f868fe109163db3286774d5590bf6a8003b38917 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 15:07:52 +1000 Subject: [PATCH 034/205] Change markup for the graph button to make it align where we want --- app/assets/javascripts/templates/shop_variant.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index 938259d80f..bf0907088e 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -52,7 +52,10 @@ %button.graph-button{"price-breakdown" => "_", "variant" => "variant", - "price-breakdown-animation" => "true"} + "ng-class" => "{open: tt_isOpen}", + "price-breakdown-animation" => "true", + "price-breakdown-append-to-body" => "true", + "price-breakdown-placement" => "left"} %i.ofn-i-058-graph .small-12.medium-2.large-2.columns.total-price.text-right From 7ad10ee0767cf435b43801ccbae4ee3c1f42d87c Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 15:08:16 +1000 Subject: [PATCH 035/205] Style popovers and price breakdowns --- .../darkswarm/_shop-popovers.css.sass | 97 +++++++++++++------ 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index fec2eae4a6..4eb562cb56 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -1,37 +1,76 @@ @import mixins -.darkswarm - product - // Pop over +// .darkswarm +// product - // Foundation overrides - .joyride-tip-guide - // JS needs to be tweaked to adjust for left alignment - this is dynamic can't rewrite in CSS - background-color: #ebebeb - border: 1px solid #a5a5a5 - color: #1f1f1f - - h1, h2, h3, h4, h5, h6 - color: #1f1f1f - .joyride-nub.bottom - border-color: #a5a5a5 !important - border-bottom-color: transparent !important - border-left-color: transparent !important - border-right-color: transparent !important +// Pop over +// Foundation overrides +.joyride-tip-guide + // JS needs to be tweaked to adjust for left alignment - this is dynamic can't rewrite in CSS + background-color: #d1d1d1 + color: #1f1f1f + @include box-shadow(0 1px 2px 0 rgba(0,0,0,0.7)) - button.graph-button - padding: 0 + + h1, h2, h3, h4, h5, h6 + color: #1f1f1f + + .joyride-nub.right + top: 40px + border-color: #d1d1d1 !important + border-top-color: transparent !important + border-right-color: transparent !important + border-bottom-color: transparent !important + + .progress + background-color: #148774 + padding: 0 + border: none + color: white + font-size: 0.75rem + font-style: oblique + line-height: 1 + height: auto + .right + padding: 0.5rem 0.25rem 0 0 + .meter + background-color: #097563 + padding: 0.5rem 0.25rem + border-right: 1px solid #539f92 + + .expanded + ul, li + list-style: none margin: 0 - @include border-radius(0) - display: inline - background: none + font-size: 0.875rem + li + background-color: #148774 + padding: 0 0.25rem + margin-bottom: 2px + color: white + li.cost + background-color: #097563 + + +button.graph-button + padding: 0 + margin: 0 + @include border-radius(99999) + display: inline + background: rgba(255,255,255,0.5) + padding: 0.2rem + &:hover, &:focus, &:active, &.active + background: $clr-brick-bright + i.ofn-i-058-graph + color: white + + i.ofn-i-058-graph + color: #999 + margin: 0 + padding: 0 + font-size: 1rem + + - i.ofn-i-058-graph - color: #999 - margin: 0 - padding: 0 - font-size: 1rem - &:hover, &:focus, &:active, &.active - color: #444 \ No newline at end of file From 042db2d150342ab58eef3533a1d0d5b83a022e78 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 15:05:25 +1000 Subject: [PATCH 036/205] Some comments and refactoring --- .../darkswarm/directives/active_selector.js.coffee | 5 ++++- .../directives/active_table_hub_link.js.coffee | 5 +++-- .../darkswarm/directives/cart_popover.js.coffee | 1 + .../darkswarm/directives/price_breakdown.js.coffee | 8 +++++++- .../javascripts/templates/price_breakdown.html.haml | 1 - .../templates/price_breakdown_button.html.haml | 2 ++ .../javascripts/templates/shop_variant.html.haml | 10 ++++------ 7 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 app/assets/javascripts/templates/price_breakdown_button.html.haml diff --git a/app/assets/javascripts/darkswarm/directives/active_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/active_selector.js.coffee index 74523c2c3d..563b5a1716 100644 --- a/app/assets/javascripts/darkswarm/directives/active_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/active_selector.js.coffee @@ -1,4 +1,6 @@ Darkswarm.directive "activeSelector", -> + # A generic selector that allows an object/scope to be toggled between active and inactive + # Used in the filters, but hypothetically useable anywhere restrict: 'E' transclude: true replace: true @@ -8,5 +10,6 @@ Darkswarm.directive "activeSelector", -> elem.bind "click", -> scope.$apply -> scope.selector.active = !scope.selector.active - scope.emit() + # This function is a convention, e.g. a callback on the scope applied when active changes + scope.emit() if scope.emit diff --git a/app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee b/app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee index 4369fced34..994a7ced48 100644 --- a/app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee @@ -1,11 +1,12 @@ Darkswarm.directive "activeTableHubLink", (CurrentHub, CurrentOrder) -> + # Change the text of the hub link based on CurrentHub + # To be used with ofnEmptiesCart + # Takes "change" and "shop" as text string attributes restrict: "A" scope: hub: '=activeTableHubLink' template: "{{action}}" link: (scope, elm, attr)-> - # Swap out the text of the hub link depending on whether it'll change current hub - # To be used with ofnEmptiesCart if CurrentHub.hub?.id and CurrentHub.hub.id isnt scope.hub.id scope.action = attr.change else diff --git a/app/assets/javascripts/darkswarm/directives/cart_popover.js.coffee b/app/assets/javascripts/darkswarm/directives/cart_popover.js.coffee index bcc0c8bc08..1ef1b3f9c3 100644 --- a/app/assets/javascripts/darkswarm/directives/cart_popover.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/cart_popover.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive "cart", -> + # Toggles visibility of the "cart" popover restrict: 'A' link: (scope, elem, attr)-> scope.open = false diff --git a/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee b/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee index 0d1d6ce0b5..8b0beb17a5 100644 --- a/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee @@ -1,9 +1,15 @@ Darkswarm.directive "priceBreakdown", ($tooltip)-> - tooltip = $tooltip 'priceBreakdown', 'priceBreakdown', 'click' + # We use the $tooltip service from Angular foundation to give us boilerplate + # Subsequently we patch the scope, template and restrictions + tooltip = $tooltip 'priceBreakdown', 'priceBreakdown', 'click' tooltip.scope = variant: "=" + tooltip.templateUrl = "price_breakdown_button.html" + tooltip.replace = true + tooltip.restrict = 'E' tooltip +# This is automatically referenced via naming convention in $tooltip Darkswarm.directive 'priceBreakdownPopup', -> restrict: 'EA' replace: true diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index f9919b92ff..9317356d02 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -1,7 +1,6 @@ .joyride-tip-guide{bindonce: true, "ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} %span.joyride-nub.right .joyride-content-wrapper - .collapsed{"ng-show" => "!expanded"} %price-percentage{percentage: 'variant.basePricePercentage'} %a{"ng-click" => "expanded = !expanded"} diff --git a/app/assets/javascripts/templates/price_breakdown_button.html.haml b/app/assets/javascripts/templates/price_breakdown_button.html.haml new file mode 100644 index 0000000000..8a86ed2618 --- /dev/null +++ b/app/assets/javascripts/templates/price_breakdown_button.html.haml @@ -0,0 +1,2 @@ +%button.graph-button{"ng-class" => "{open: tt_isOpen}"} + %i.ofn-i-058-graph diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index bf0907088e..a6db8b4e41 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -50,13 +50,11 @@ %i.ofn-i_009-close {{ variant.price | currency }} - %button.graph-button{"price-breakdown" => "_", - "variant" => "variant", - "ng-class" => "{open: tt_isOpen}", - "price-breakdown-animation" => "true", + -# Now in a template in app/assets/javascripts/templates ! + %price-breakdown{"price-breakdown" => "_", variant: "variant", "price-breakdown-append-to-body" => "true", - "price-breakdown-placement" => "left"} - %i.ofn-i-058-graph + "price-breakdown-placement" => "left", + "price-breakdown-animation" => true} .small-12.medium-2.large-2.columns.total-price.text-right .table-cell From 647495465a7e4e68a3f8d86af9a411578614caf1 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 15:12:25 +1000 Subject: [PATCH 037/205] Comments and minor refactoring --- .../darkswarm/directives/debounce.js.coffee | 2 ++ .../directives/disable_enter.js.coffee | 1 + .../directives/disable_scroll.js.coffee | 3 ++- .../directives/empties_cart.js.coffee | 21 ++++++++++--------- .../darkswarm/services/cart.js.coffee | 3 +++ .../services/current_order.js.coffee | 2 -- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/darkswarm/directives/debounce.js.coffee b/app/assets/javascripts/darkswarm/directives/debounce.js.coffee index 343fcb531a..80f55de1d7 100644 --- a/app/assets/javascripts/darkswarm/directives/debounce.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/debounce.js.coffee @@ -1,4 +1,6 @@ Darkswarm.directive "ngDebounce", ($timeout) -> + # Slows down ng-model updates, only triggering binding ngDebounce milliseconds + # after the last change. Used to prevent squirrely UI restrict: "A" require: "ngModel" priority: 99 diff --git a/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee b/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee index 352ed85fd6..ce1178e15e 100644 --- a/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive "ofnDisableEnter", ()-> + # Stops enter from doing normal enter things restrict: 'A' link: (scope, element, attrs)-> element.bind "keydown keypress", (e)-> diff --git a/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee b/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee index 7c870ab860..40b8230e65 100644 --- a/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee @@ -1,6 +1,7 @@ Darkswarm.directive "ofnDisableScroll", ()-> + # Stops scrolling from incrementing or decrementing input value + # Useful for number inputs restrict: 'A' - link: (scope, element, attrs)-> element.bind 'focus', -> element.bind 'mousewheel', (e)-> diff --git a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee b/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee index 75e88f3682..b49131afa6 100644 --- a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee @@ -1,12 +1,13 @@ -Darkswarm.directive "ofnEmptiesCart", (CurrentHub, CurrentOrder, Navigation, storage) -> +Darkswarm.directive "ofnEmptiesCart", (CurrentHub, Cart, Navigation, storage) -> + # Compares scope.hub with CurrentHub. Will trigger an confirmation if they are different, + # and Cart isn't empty restrict: "A" + scope: + hub: "=ofnEmptiesCart" link: (scope, elm, attr)-> - hub = scope.$eval(attr.ofnEmptiesCart) - # A hub is selected, we're changing to a different hub, and the cart isn't empty - if CurrentHub.hub?.id and CurrentHub.hub.id isnt hub.id - unless CurrentOrder.empty() - elm.bind 'click', (ev)-> - ev.preventDefault() - if confirm "Are you sure? This will change your selected Hub and remove any items in you shopping cart." - storage.clearAll() # One day this will have to be moar GRANULAR - Navigation.go scope.hub.path + if CurrentHub.hub?.id and CurrentHub.hub.id isnt scope.hub.id and !Cart.empty() + elm.bind 'click', (ev)-> + ev.preventDefault() + if confirm "Are you sure? This will change your selected Hub and remove any items in you shopping cart." + storage.clearAll() # One day this will have to be moar GRANULAR + Navigation.go scope.hub.path diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index aa6820f150..def1c9b6d0 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -43,6 +43,9 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> @line_items.filter (li)-> li.quantity > 0 + empty: => + @line_items_present().length == 0 + total: => @line_items_present().map (li)-> li.variant.getPrice() diff --git a/app/assets/javascripts/darkswarm/services/current_order.js.coffee b/app/assets/javascripts/darkswarm/services/current_order.js.coffee index a0714dc005..cd2402f0c0 100644 --- a/app/assets/javascripts/darkswarm/services/current_order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/current_order.js.coffee @@ -1,5 +1,3 @@ Darkswarm.factory 'CurrentOrder', (currentOrder) -> new class CurrentOrder order: currentOrder - empty: => - @order.line_items.length == 0 From 494b572c69456e7c0cfc73c38aa2ddc99d4d46d4 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 31 Jul 2014 15:36:13 +1000 Subject: [PATCH 038/205] Do not show zero fees --- lib/open_food_network/enterprise_fee_calculator.rb | 2 +- .../lib/open_food_network/enterprise_fee_calculator_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb index 2d76d8d280..c8070f2ab2 100644 --- a/lib/open_food_network/enterprise_fee_calculator.rb +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -19,7 +19,7 @@ module OpenFoodNetwork fees[applicator.enterprise_fee.fee_type.to_sym] ||= 0 fees[applicator.enterprise_fee.fee_type.to_sym] += calculate_fee_for variant, applicator fees - end + end.select { |fee_type, amount| amount > 0 } end diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb index 21c4848fda..0ca7da4999 100644 --- a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -45,6 +45,12 @@ module OpenFoodNetwork it "returns a breakdown of fees" do EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12} end + + it "filters out zero fees" do + ef_admin.calculator.update_attribute :preferred_amount, 0 + + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {sales: 4.56, packing: 7.89, transport: 0.12} + end end describe "creating adjustments" do From b0f15aef104dbf62532330231c4ce8e3004d9d5e Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 15:38:12 +1000 Subject: [PATCH 039/205] Popovers finish styling for various use cases --- .../darkswarm/_shop-popovers.css.sass | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index 4eb562cb56..4485819324 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -8,17 +8,23 @@ // Foundation overrides .joyride-tip-guide // JS needs to be tweaked to adjust for left alignment - this is dynamic can't rewrite in CSS - background-color: #d1d1d1 + background-color: #999 color: #1f1f1f @include box-shadow(0 1px 2px 0 rgba(0,0,0,0.7)) - + .joyride-content-wrapper + padding: 1.125rem 1.25rem 1.5rem + padding: 1rem + margin: 1% + width: 98% + background-color: white + h1, h2, h3, h4, h5, h6 color: #1f1f1f .joyride-nub.right top: 40px - border-color: #d1d1d1 !important + border-color: #999 !important border-top-color: transparent !important border-right-color: transparent !important border-bottom-color: transparent !important @@ -51,6 +57,8 @@ color: white li.cost background-color: #097563 + li:last-child + margin-bottom: 0.75rem button.graph-button @@ -58,12 +66,18 @@ button.graph-button margin: 0 @include border-radius(99999) display: inline - background: rgba(255,255,255,0.5) + background-color: rgba(255,255,255,0.5) padding: 0.2rem - &:hover, &:focus, &:active, &.active - background: $clr-brick-bright + + &:focus + background-color: rgba(255,255,255,0.5) i.ofn-i-058-graph - color: white + color: #999 + + &:hover, &:active + background-color: rgba(255,255,255,1) + i.ofn-i-058-graph + color: $clr-brick-bright i.ofn-i-058-graph color: #999 @@ -71,6 +85,16 @@ button.graph-button padding: 0 font-size: 1rem +button.graph-button.open + background-color: #999 + + &:hover, &:active, &:focus + background-color: #b2b2b2 + i.ofn-i-058-graph + color: $clr-brick-bright + + i.ofn-i-058-graph + color: $clr-brick From 7ca618fb2925ff33fdf1a13dc4fdb776a8023b19 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 11:11:34 +1000 Subject: [PATCH 040/205] Moving to state_name --- app/serializers/api/address_serializer.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/serializers/api/address_serializer.rb b/app/serializers/api/address_serializer.rb index 18483da312..66e4267e1f 100644 --- a/app/serializers/api/address_serializer.rb +++ b/app/serializers/api/address_serializer.rb @@ -1,10 +1,12 @@ class Api::AddressSerializer < ActiveModel::Serializer - cached - delegate :cache_key, to: :object + #cached + #delegate :cache_key, to: :object - attributes :id, :zipcode, :city, :state, :state_id + attributes :id, :zipcode, :city, :state_name, :state_id, + :phone, :firstname, :lastname, :address1, :address2, :city, :country_id, + :zipcode - def state + def state_name object.state.andand.abbr end end From 5cff2f0f403e33eeb2ed164116e57e6f9f2d3315 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 31 Jul 2014 15:47:54 +1000 Subject: [PATCH 041/205] Tweak colors for popovers --- app/assets/stylesheets/darkswarm/_shop-popovers.css.sass | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index 4485819324..b79350818f 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -30,7 +30,7 @@ border-bottom-color: transparent !important .progress - background-color: #148774 + background-color: #13bf85 padding: 0 border: none color: white @@ -41,7 +41,7 @@ .right padding: 0.5rem 0.25rem 0 0 .meter - background-color: #097563 + background-color: #0b8c61 padding: 0.5rem 0.25rem border-right: 1px solid #539f92 @@ -51,12 +51,12 @@ margin: 0 font-size: 0.875rem li - background-color: #148774 + background-color: #13bf85 padding: 0 0.25rem margin-bottom: 2px color: white li.cost - background-color: #097563 + background-color: #0b8c61 li:last-child margin-bottom: 0.75rem From 6b00b1c14c2a5e9169c951c347c1b87e65c6de44 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 31 Jul 2014 15:38:15 +1000 Subject: [PATCH 042/205] MOAR COMMENTS --- .../directives/fill_vertical.js.coffee | 3 +-- .../darkswarm/directives/flash.js.coffee | 6 +++++- .../darkswarm/directives/focus.js.coffee | 6 +++--- .../darkswarm/directives/loading.js.coffee | 3 +-- .../darkswarm/directives/modal.js.coffee | 17 ++++++++++------- .../darkswarm/directives/render_svg.js.coffee | 4 ++++ .../directives/scroll_after_load.js.coffee | 1 + .../darkswarm/directives/scrollto.js.coffee | 2 ++ .../directives/shipping_type_selector.js.coffee | 1 + .../directives/taxon_selector.js.coffee | 11 +++++++++-- 10 files changed, 37 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/darkswarm/directives/fill_vertical.js.coffee b/app/assets/javascripts/darkswarm/directives/fill_vertical.js.coffee index a8a2fbeebc..c9d05428ce 100644 --- a/app/assets/javascripts/darkswarm/directives/fill_vertical.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/fill_vertical.js.coffee @@ -1,10 +1,9 @@ Darkswarm.directive "fillVertical", ($window)-> + # Makes something fill the window vertically. Used on the Google Map. restrict: 'A' - link: (scope, element, attrs)-> setSize = -> element.css "height", ($window.innerHeight - element.offset().top) setSize() - angular.element($window).bind "resize", -> setSize() diff --git a/app/assets/javascripts/darkswarm/directives/flash.js.coffee b/app/assets/javascripts/darkswarm/directives/flash.js.coffee index b5c9aaddd8..74f6571f73 100644 --- a/app/assets/javascripts/darkswarm/directives/flash.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/flash.js.coffee @@ -1,5 +1,6 @@ Darkswarm.directive "ofnFlash", (flash, $timeout, RailsFlashLoader)-> - # Mappings between flash types (left) and Foundation classes + # Our own flash class. Uses the "flash" service (third party), and a directive + # called RailsFlashLoader to render typePairings = info: "info" error: "alert" @@ -13,6 +14,8 @@ Darkswarm.directive "ofnFlash", (flash, $timeout, RailsFlashLoader)-> link: ($scope, element, attr) -> $scope.flashes = [] + + # Callback when a new flash message is pushed to flash service show = (message, type)=> if message $scope.flashes.push({message: message, type: typePairings[type]}) @@ -21,5 +24,6 @@ Darkswarm.directive "ofnFlash", (flash, $timeout, RailsFlashLoader)-> $scope.delete = -> $scope.flashes.shift() + # Register our callback (above) with flash service flash.subscribe(show) RailsFlashLoader.initFlash() diff --git a/app/assets/javascripts/darkswarm/directives/focus.js.coffee b/app/assets/javascripts/darkswarm/directives/focus.js.coffee index c481702d6c..3c3bb4f05a 100644 --- a/app/assets/javascripts/darkswarm/directives/focus.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/focus.js.coffee @@ -1,9 +1,9 @@ Darkswarm.directive "ofnFocus", -> + # Takes an expression attrs.ofnFocus + # Watches value of expression, triggers element.focus() when value is truthy + # Used to automatically focus on specific inputs in various circumstances restrict: "A" link: (scope, element, attrs) -> scope.$watch attrs.ofnFocus, ((focus) -> focus and element.focus() - return ), true - - return diff --git a/app/assets/javascripts/darkswarm/directives/loading.js.coffee b/app/assets/javascripts/darkswarm/directives/loading.js.coffee index febe56de9b..c7191b4fe2 100644 --- a/app/assets/javascripts/darkswarm/directives/loading.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/loading.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive "loading", (Loading)-> + # Triggers a screen-wide "loading" thing when Ajaxy stuff is happening scope: {} restrict: 'E' templateUrl: 'loading.html' @@ -6,5 +7,3 @@ Darkswarm.directive "loading", (Loading)-> $scope.Loading = Loading $scope.show = -> $scope.Loading.message? - - link: ($scope, element, attr)-> diff --git a/app/assets/javascripts/darkswarm/directives/modal.js.coffee b/app/assets/javascripts/darkswarm/directives/modal.js.coffee index d727fa64c9..7c1babe215 100644 --- a/app/assets/javascripts/darkswarm/directives/modal.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/modal.js.coffee @@ -1,16 +1,19 @@ Darkswarm.directive "ofnModal", ($modal)-> + # Generic modal! Uses transclusion so designer-types can do stuff like: + # %ofn-modal + # CONTENT + # Only works for simple cases, so roll your own when necessary! restrict: 'E' replace: true transclude: true - scope: {} + scope: true template: "{{title}}" + # Instead of using ng-transclude we compile the transcluded template to a string + # This compiled template is sent to the $modal service! Such magic! + # In theory we could compile the template directly inside link rather than onclick, but it's performant so meh! link: (scope, elem, attrs, ctrl, transclude)-> scope.title = attrs.title - contents = null elem.on "click", => - # We're using an isolate scope, which is a child of the original scope - # We have to compile the transclude against the original scope, not the isolate - transclude scope.$parent, (clone)-> - contents = clone - scope.modalInstance = $modal.open(controller: ctrl, template: contents, scope: scope.$parent) + transclude scope, (clone)-> + scope.modalInstance = $modal.open(controller: ctrl, template: clone, scope: scope) diff --git a/app/assets/javascripts/darkswarm/directives/render_svg.js.coffee b/app/assets/javascripts/darkswarm/directives/render_svg.js.coffee index 86ffea8c5b..c53448473d 100644 --- a/app/assets/javascripts/darkswarm/directives/render_svg.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/render_svg.js.coffee @@ -1,7 +1,11 @@ Darkswarm.directive "renderSvg", ()-> + # Magical directive that'll render SVGs from URLs + # If only there were a neater way of doing this restrict: 'E' priority: 99 template: "" + + # Fetch SVG via ajax, inject into page using DOM link: (scope, elem, attr)-> if /.svg/.test attr.path # Only do this if we've got an svg $.ajax diff --git a/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee b/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee index ab2e0a6e3d..55c5a311da 100644 --- a/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive 'scrollAfterLoad', ($timeout, $location, $document)-> + # Scroll to an element on page load restrict: "A" link: (scope, element, attr) -> if scope.$last is true diff --git a/app/assets/javascripts/darkswarm/directives/scrollto.js.coffee b/app/assets/javascripts/darkswarm/directives/scrollto.js.coffee index 3db3446596..a63337dc70 100644 --- a/app/assets/javascripts/darkswarm/directives/scrollto.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/scrollto.js.coffee @@ -1,4 +1,6 @@ Darkswarm.directive "ofnScrollTo", ($location, $anchorScroll)-> + # Onclick sets $location.hash to attrs.ofnScrollTo + # Then triggers anchorScroll restrict: 'A' link: (scope, element, attrs)-> element.bind 'click', (ev)-> diff --git a/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee index 3f752f8a3d..07656d84b7 100644 --- a/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive "shippingTypeSelector", (FilterSelectorsService)-> + # Builds selector for shipping types restrict: 'E' replace: true templateUrl: 'shipping_type_selector.html' diff --git a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee index a682176d77..633ecd22f4 100644 --- a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee @@ -1,4 +1,6 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> + # Automatically builds activeSelectors for taxons + # Lots of magic here restrict: 'E' replace: true scope: @@ -8,7 +10,7 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> link: (scope, elem, attr)-> selectors_by_id = {} - selectors = ["foo"] + selectors = null # To get scoping/closure right scope.emit = -> scope.results = selectors.filter (selector)-> @@ -16,6 +18,7 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> .map (selector)-> selector.taxon.id + # Build hash of unique taxons, each of which gets an ActiveSelector scope.selectors = -> taxons = {} selectors = [] @@ -25,7 +28,11 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> if object.supplied_taxons for taxon in object.supplied_taxons taxons[taxon.id] = taxon - + + # Generate a selector for each taxon. + # NOTE: THESE ARE MEMOIZED to stop new selectors from being created constantly, otherwise function always returns non-identical results + # This means the $digest cycle can never close and times out + # See http://stackoverflow.com/questions/19306452/how-to-fix-10-digest-iterations-reached-aborting-error-in-angular-1-2-fil for id, taxon of taxons if selector = selectors_by_id[id] selectors.push selector From d254a8f3d913c72509ef8f20a000d319f2bf797d Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 11:35:29 +1000 Subject: [PATCH 043/205] Tweak styling to bring back hover states against product row for shopfront view --- .../darkswarm/_shop-product-rows.css.sass | 15 ++++++++------- .../stylesheets/darkswarm/branding.css.sass | 3 ++- app/assets/stylesheets/darkswarm/shop.css.sass | 5 +++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index fbe5949e74..c2aadf5a46 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -1,3 +1,5 @@ +@import branding.css.sass + .darkswarm products product @@ -25,9 +27,13 @@ .row.variants margin-left: 0 margin-right: 0 - background: url("/assets/gray_jean.png") top left repeat + background-color: #ECECEC + &:hover, &:focus, &:active + background-color: $clr-brick-light &:nth-of-type(even) - background: url("/assets/gray_jean_light.png") top left repeat + background-color: #f9f9f9 + &:hover, &:focus, &:active + background-color: $clr-brick-ultra-light // Variant name .variant-name @@ -72,7 +78,6 @@ .table-cell height: 27px - // ROW SUMMARY .row.summary margin-left: 0 @@ -108,7 +113,3 @@ - - - - diff --git a/app/assets/stylesheets/darkswarm/branding.css.sass b/app/assets/stylesheets/darkswarm/branding.css.sass index 737915086b..68f60dde53 100644 --- a/app/assets/stylesheets/darkswarm/branding.css.sass +++ b/app/assets/stylesheets/darkswarm/branding.css.sass @@ -8,12 +8,13 @@ // $clr-turquoise: #097563 // $clr-turquoise-light: #cef2ec // $clr-turquoise-ultra-light: #e6faf7 -// $clr-turquoise-bright: #1d8f7c +// $clr-turquoise-bright: #1d8f7c $clr-brick: #c1122b $clr-brick-light: #f5e6e7 $clr-brick-ultra-light: #faf5f6 $clr-brick-bright: #eb4c46 +$clr-brick-med-bright: #e5a2a0 $clr-brick-light-bright: #f5c4c9 $clr-turquoise: #0b8c61 diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 70b3997825..1fa3f55c36 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -38,6 +38,11 @@ display: block color: #444 + &:hover, &:focus, &:active + border-bottom: 1px solid $clr-brick-med-bright + border-top: 1px solid $clr-brick-med-bright + @include box-shadow(0 0 5px 0 #f5c4c9) + // BULK .bulk-buy font-size: 0.875rem From 9ba8eb8bae01a112d5f7efe5a688297342b7cce8 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 11:49:29 +1000 Subject: [PATCH 044/205] Add animation easing and remove glow from product level highlight --- app/assets/stylesheets/darkswarm/shop.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 1fa3f55c36..a2df3706c7 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -30,6 +30,7 @@ product + @include csstrans border-bottom: 1px solid #e5e5e5 border-top: 1px solid #e5e5e5 padding-bottom: 1px @@ -41,7 +42,6 @@ &:hover, &:focus, &:active border-bottom: 1px solid $clr-brick-med-bright border-top: 1px solid $clr-brick-med-bright - @include box-shadow(0 0 5px 0 #f5c4c9) // BULK .bulk-buy From c3452b178b70d9560ceceed1c129f56b90812f07 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 12:00:04 +1000 Subject: [PATCH 045/205] Styling popovers so the price breakdown is different to the shopping cart popover --- app/assets/javascripts/templates/price_breakdown.html.haml | 2 +- app/assets/stylesheets/darkswarm/_shop-popovers.css.sass | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index 9317356d02..724aa1ad63 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -1,4 +1,4 @@ -.joyride-tip-guide{bindonce: true, "ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} +.joyride-tip-guide.price_breakdown{bindonce: true, "ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} %span.joyride-nub.right .joyride-content-wrapper .collapsed{"ng-show" => "!expanded"} diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index b79350818f..3e89c2bd18 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -6,7 +6,7 @@ // Pop over // Foundation overrides -.joyride-tip-guide +.joyride-tip-guide.price_breakdown // JS needs to be tweaked to adjust for left alignment - this is dynamic can't rewrite in CSS background-color: #999 color: #1f1f1f From 2c95f00485877bef90bcdbfc8d1868bf39f5fadf Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 12:02:10 +1000 Subject: [PATCH 046/205] Adding T&Cs into footer link --- app/views/shared/_footer.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index afe27bc02f..4160d801a7 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -43,7 +43,7 @@ %a{href:"https://creativecommons.org/licenses/by-sa/3.0/", target: "_blank" } Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) %p %small - %a{href:'' } Site terms & conditions + %a{href:"http://openfoodfoundation.org/library/ofn-terms-service", target: "_blank" } Site terms & conditions | %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank" } Open Source & developer info on Github From e91e638d357fffebe13c81936cb8cfc45a1f8a8f Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 12:21:08 +1000 Subject: [PATCH 047/205] Tweaking cart summary view markup --- app/views/checkout/_summary.html.haml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index 60fb8ca80a..a573410ccf 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -1,22 +1,22 @@ %orderdetails = form_for current_order, url: "#", html: {"ng-submit" => "purchase($event)"} do |f| %fieldset - %legend Your Order + %legend Your order %table %tr - %th Produce - %td= current_order.display_item_total + %th Items + %td.text-right= current_order.display_item_total - - checkout_adjustments_for_summary(current_order, exclude_shipping: true).each do |adjustment| - %tr - %th= adjustment.label - %td= adjustment.display_amount.to_html + / - checkout_adjustments_for_summary(current_order, exclude_shipping: true).each do |adjustment| + / %tr + / %th= adjustment.label + / %td.text-right= adjustment.display_amount.to_html %tr %th Shipping - %td {{ Checkout.shippingPrice() | currency }} + %td.text-right {{ Checkout.shippingPrice() | currency }} %tr - %th Cart total - %td {{ Checkout.cartTotal() | currency }} + %th Total + %td.text-right {{ Checkout.cartTotal() | currency }} - if current_order.price_adjustment_totals.present? - current_order.price_adjustment_totals.each do |label, total| %tr From 2fca9ed3e95d849de0cad4a71dc28bdad3b64564 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 12:49:49 +1000 Subject: [PATCH 048/205] Tweak blues for success button style --- app/assets/stylesheets/darkswarm/ui.css.sass | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/ui.css.sass b/app/assets/stylesheets/darkswarm/ui.css.sass index 9d28e16787..a4d70392bb 100644 --- a/app/assets/stylesheets/darkswarm/ui.css.sass +++ b/app/assets/stylesheets/darkswarm/ui.css.sass @@ -58,10 +58,10 @@ text-shadow: 0 1px 0 $clr-brick button.success, .button.success - background: $clr-turquoise + background: #0096ad .button.success:hover, .button.success:active, .button.success:focus, button.success:hover, button.success:active, button.success:focus - background: $clr-turquoise-bright + background: #14b6cc // Responsive @media screen and (min-width: 768px) From 3475506ad0a8b5eec85b1121f853980dd8488fc6 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 12:57:16 +1000 Subject: [PATCH 049/205] Remove the logic around fees breakdown as not showing on this view now --- app/views/checkout/_summary.html.haml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index a573410ccf..91824b8864 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -17,11 +17,11 @@ %tr %th Total %td.text-right {{ Checkout.cartTotal() | currency }} - - if current_order.price_adjustment_totals.present? - - current_order.price_adjustment_totals.each do |label, total| - %tr - %th= label - %td= total + / - if current_order.price_adjustment_totals.present? + / - current_order.price_adjustment_totals.each do |label, total| + / %tr + / %th= label + / %td= total //= f.submit "Purchase", class: "button", "ng-disabled" => "checkout.$invalid", "ofn-focus" => "accordion['payment']" %a.button.secondary{href: cart_url} From aaae8dce2d019e7e38be7702e34dac3a4e1d3dab Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 12:57:44 +1000 Subject: [PATCH 050/205] Move button out of UL in nesting --- app/views/shared/menu/_cart.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index 907fda0eff..a5e585ff37 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -36,5 +36,5 @@ .columns.small-6.text-right %strong {{ Cart.total() | currency }} - .text-right - %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty"} Checkout now + .text-right + %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty"} Checkout now From 66f1a672a3a436868d23891ada633ab8bd18f829 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 13:02:37 +1000 Subject: [PATCH 051/205] Change Cost label to Item Cost for price breakdowns --- app/assets/javascripts/templates/price_breakdown.html.haml | 2 +- app/assets/javascripts/templates/price_percentage.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index 724aa1ad63..01588933ca 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -11,7 +11,7 @@ %ul %li.cost .right {{ variant.base_price | currency }} - Cost + Item cost %li{"bo-if" => "variant.fees.admin"} .right {{ variant.fees.admin | currency }} Admin fee diff --git a/app/assets/javascripts/templates/price_percentage.html.haml b/app/assets/javascripts/templates/price_percentage.html.haml index 4982f63eb7..e577d86d7a 100644 --- a/app/assets/javascripts/templates/price_percentage.html.haml +++ b/app/assets/javascripts/templates/price_percentage.html.haml @@ -1,5 +1,5 @@ .progress .right Fees .meter - Cost + Item cost From 1ca9c60c8485599574fc074d896d1404368e9d29 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 13:18:58 +1000 Subject: [PATCH 052/205] Tweak label to make a distinction between go straight to checkout and just go to shopping cart review --- app/views/shared/menu/_cart.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index a5e585ff37..44047e5261 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -37,4 +37,4 @@ %strong {{ Cart.total() | currency }} .text-right - %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty"} Checkout now + %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty"} Quick checkout From d48b317d3b1541e215c5c962e7f0cedce869e43e Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 13:25:06 +1000 Subject: [PATCH 053/205] Checkout page header update --- app/views/spree/orders/edit.html.haml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/views/spree/orders/edit.html.haml b/app/views/spree/orders/edit.html.haml index 4fdeea348b..41e808df90 100644 --- a/app/views/spree/orders/edit.html.haml +++ b/app/views/spree/orders/edit.html.haml @@ -2,12 +2,14 @@ .darkswarm - content_for :order_cycle_form do - %strong.avenir + %closing Your shopping cart + %p Order ready on - - if @order.order_cycle - = pickup_time @order.order_cycle - - else - = @order.distributor.next_collection_at + %strong + - if @order.order_cycle + = pickup_time @order.order_cycle + - else + = @order.distributor.next_collection_at = render partial: "shopping_shared/details" From 6515f0f09650791b8faa704fc94a75539cece870 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 13:34:06 +1000 Subject: [PATCH 054/205] Tweak labels to make page heading more consistent, give useful names to SHopping cart and Checkout pages --- app/views/checkout/_form.html.haml | 6 +++--- app/views/checkout/edit.html.haml | 7 +++++-- app/views/spree/orders/edit.html.haml | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index 68aaf2d781..aa2312ccee 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -10,9 +10,9 @@ angular.module('Darkswarm').value('order', #{render "checkout/order"}) %div - %h3.text-center.pad-top - Checkout from - = current_distributor.name + / %h3.text-center.pad-top + / Checkout from + / = current_distributor.name = render partial: "checkout/details", locals: {f: f} = render partial: "checkout/billing", locals: {f: f} diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index 156f435970..34a780020d 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -2,9 +2,12 @@ .darkswarm - content_for :order_cycle_form do - %strong.avenir + + %closing Checkout + %p Order ready for - = pickup_time current_order_cycle + %strong + = pickup_time current_order_cycle = render partial: "shopping_shared/details" diff --git a/app/views/spree/orders/edit.html.haml b/app/views/spree/orders/edit.html.haml index 41e808df90..5f8186e568 100644 --- a/app/views/spree/orders/edit.html.haml +++ b/app/views/spree/orders/edit.html.haml @@ -4,7 +4,7 @@ - content_for :order_cycle_form do %closing Your shopping cart %p - Order ready on + Order ready for %strong - if @order.order_cycle = pickup_time @order.order_cycle From 5d2dda2ee430f3c324165c6d881dfbc751261fcb Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 13:34:35 +1000 Subject: [PATCH 055/205] Tweak checkout header --- app/views/checkout/edit.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index 34a780020d..1c4932c481 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -3,7 +3,7 @@ .darkswarm - content_for :order_cycle_form do - %closing Checkout + %closing Checkout now %p Order ready for %strong From 5a34fa41b3d123ee1dfa795ca987df5c9d7c8732 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 6 Aug 2014 13:58:52 +1000 Subject: [PATCH 056/205] Fixing cart total in checkout --- app/serializers/api/current_order_serializer.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/serializers/api/current_order_serializer.rb b/app/serializers/api/current_order_serializer.rb index 6275710789..9f939b09c1 100644 --- a/app/serializers/api/current_order_serializer.rb +++ b/app/serializers/api/current_order_serializer.rb @@ -10,4 +10,8 @@ class Api::CurrentOrderSerializer < ActiveModel::Serializer def payment_method_id object.payments.first.andand.payment_method_id end + + def display_total + object.display_total.money.to_f + end end From 099e2257ca0b6a33a11b63a136c170712084b5a4 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 14:30:37 +1000 Subject: [PATCH 057/205] Adding width and height into SVGs to fix the firefox bug --- app/assets/images/map-icon-both.svg | 2 +- app/assets/images/map-icon-hub.svg | 2 +- app/assets/images/map-icon-producer.svg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/images/map-icon-both.svg b/app/assets/images/map-icon-both.svg index 08ffa25f6c..a51867f841 100644 --- a/app/assets/images/map-icon-both.svg +++ b/app/assets/images/map-icon-both.svg @@ -1,7 +1,7 @@ - diff --git a/app/assets/images/map-icon-hub.svg b/app/assets/images/map-icon-hub.svg index bb0e20aec4..fcd6a4913e 100644 --- a/app/assets/images/map-icon-hub.svg +++ b/app/assets/images/map-icon-hub.svg @@ -1,7 +1,7 @@ - diff --git a/app/assets/images/map-icon-producer.svg b/app/assets/images/map-icon-producer.svg index 1f2e4184a1..50ec792e91 100644 --- a/app/assets/images/map-icon-producer.svg +++ b/app/assets/images/map-icon-producer.svg @@ -1,7 +1,7 @@ - From de725a4cd68bed38fe7997272d40d6e9fb3aa638 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 14:33:42 +1000 Subject: [PATCH 058/205] Add unti to border radius --- app/assets/stylesheets/darkswarm/modals.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index 086ab8b076..a4a896942b 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -26,7 +26,7 @@ dialog .close-reveal-modal, .reveal-modal .close-reveal-modal background-color: rgba(235,235,235,0.85) text-shadow: none padding: 0.3rem - @include border-radius(999999) + @include border-radius(999999rem) &:hover, &:active, &:focus background-color: rgba(235,235,235,1) color: #333 From 581cf442a22448e0b831befaca23766379746f95 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 16:24:47 +1000 Subject: [PATCH 059/205] Add ToS as a PDF attachment rather than an external site --- app/views/shared/_footer.html.haml | 2 +- public/Terms-of-service.pdf | Bin 0 -> 135573 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 public/Terms-of-service.pdf diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index 4160d801a7..f8fa116f5a 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -43,7 +43,7 @@ %a{href:"https://creativecommons.org/licenses/by-sa/3.0/", target: "_blank" } Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) %p %small - %a{href:"http://openfoodfoundation.org/library/ofn-terms-service", target: "_blank" } Site terms & conditions + %a{href:"/Terms-of-service.pdf", target: "_blank" } Site terms & conditions | %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank" } Open Source & developer info on Github diff --git a/public/Terms-of-service.pdf b/public/Terms-of-service.pdf new file mode 100644 index 0000000000000000000000000000000000000000..603ffc8f86c1d96dc911f59fc4ee125b25b10665 GIT binary patch literal 135573 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≪K~Sz(dkV63|u!w*~YzF+d8jk_GS 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 Date: Wed, 6 Aug 2014 16:25:38 +1000 Subject: [PATCH 060/205] Order cycle popover make strong red and left aligned so it works when used for multiple order cycle use-cases --- .../stylesheets/darkswarm/_shop-popovers.css.sass | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index 3e89c2bd18..685aa284da 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -1,8 +1,20 @@ @import mixins +@import branding // .darkswarm // product +ordercycle + .joyride-tip-guide + background-color: $clr-brick + .joyride-nub.right + border-color: $clr-brick !important + border-top-color: transparent !important + border-right-color: transparent !important + border-bottom-color: transparent !important + p + margin: 0 + font-weight: 700 // Pop over // Foundation overrides @@ -64,7 +76,7 @@ button.graph-button padding: 0 margin: 0 - @include border-radius(99999) + @include border-radius(99999rem) display: inline background-color: rgba(255,255,255,0.5) padding: 0.2rem From 3c40ce1097adf0595c7d14071eef9a5763c524df Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 16:26:07 +1000 Subject: [PATCH 061/205] Add a unit to border-radius --- app/assets/stylesheets/darkswarm/hub_node.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/hub_node.css.sass b/app/assets/stylesheets/darkswarm/hub_node.css.sass index de0b1ca58c..d0b5dcc0ef 100644 --- a/app/assets/stylesheets/darkswarm/hub_node.css.sass +++ b/app/assets/stylesheets/darkswarm/hub_node.css.sass @@ -12,7 +12,7 @@ //Hub icon styline i.ofn-i_040-hub - @include border-radius(9999em) + @include border-radius(99999rem) font-size: 1.15rem display: inline-block padding: 0.2rem From 3de0f7893d168667345f86705cf660d40441ab82 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 16:26:32 +1000 Subject: [PATCH 062/205] Change the language and layout of order cycle popover --- app/views/shop/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shop/show.html.haml b/app/views/shop/show.html.haml index 4057823d52..048dd13e73 100644 --- a/app/views/shop/show.html.haml +++ b/app/views/shop/show.html.haml @@ -15,7 +15,7 @@ %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", "ng-change" => "changeOrderCycle()", "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", - "popover-placement" => "bottom", "popover" => "When do you want to get your order?", "popover-trigger" => "openTrigger"} + "popover-placement" => "left", "popover" => "Choose when you want your order:", "popover-trigger" => "openTrigger"} From 65b7cadf097793cdb66b2a554cea1e939ed89140 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 16:28:22 +1000 Subject: [PATCH 063/205] Change label for next button to Go to my cart --- app/views/shop/products/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index a10edd7fb9..0813d2dae0 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -12,7 +12,7 @@ %form{action: cart_path} .small-12.medium-4.large-3.columns - %input.button.primary.right.add_to_cart{type: :submit, value: "Checkout now", + %input.button.primary.right.add_to_cart{type: :submit, value: "Go to my cart", "ng-disabled" => "Cart.dirty"} %div.pad-top{bindonce: true} @@ -38,6 +38,6 @@ .row .small-12.columns %form{action: cart_path} - %input.button.primary.right.add_to_cart{type: :submit, value: "Checkout now", + %input.button.primary.right.add_to_cart{type: :submit, value: "Go to my cart", "ng-disabled" => "Cart.dirty"} From 280f6f856bb58e6eac4b3bf6eebccce08c35570a Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 16:36:08 +1000 Subject: [PATCH 064/205] update go to shopping cart button label try again --- app/views/shop/products/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 0813d2dae0..80c934d7e5 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -12,7 +12,7 @@ %form{action: cart_path} .small-12.medium-4.large-3.columns - %input.button.primary.right.add_to_cart{type: :submit, value: "Go to my cart", + %input.button.primary.right.add_to_cart{type: :submit, value: "Shopping cart >", "ng-disabled" => "Cart.dirty"} %div.pad-top{bindonce: true} @@ -38,6 +38,6 @@ .row .small-12.columns %form{action: cart_path} - %input.button.primary.right.add_to_cart{type: :submit, value: "Go to my cart", + %input.button.primary.right.add_to_cart{type: :submit, value: "Shopping cart >", "ng-disabled" => "Cart.dirty"} From b06d08015dedf3acd165852169b6684cfee11195 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 16:36:25 +1000 Subject: [PATCH 065/205] Tweak summary label --- app/views/checkout/_summary.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index 91824b8864..bc0a0e307f 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -4,7 +4,7 @@ %legend Your order %table %tr - %th Items + %th Cart total %td.text-right= current_order.display_item_total / - checkout_adjustments_for_summary(current_order, exclude_shipping: true).each do |adjustment| From cde1bcb1f5147121faa138a12da887de06806c74 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 16:58:49 +1000 Subject: [PATCH 066/205] Styling for popovers on graph - price breakdown --- .../stylesheets/darkswarm/_shop-popovers.css.sass | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index 685aa284da..5462469660 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -74,19 +74,15 @@ ordercycle button.graph-button + @include box-shadow(none) padding: 0 margin: 0 @include border-radius(99999rem) display: inline background-color: rgba(255,255,255,0.5) - padding: 0.2rem + padding: 0.35rem - &:focus - background-color: rgba(255,255,255,0.5) - i.ofn-i-058-graph - color: #999 - - &:hover, &:active + &:hover, &:active, &:focus background-color: rgba(255,255,255,1) i.ofn-i-058-graph color: $clr-brick-bright @@ -98,10 +94,10 @@ button.graph-button font-size: 1rem button.graph-button.open - background-color: #999 + @include box-shadow(inset 0 1px 1px 0 rgba(0,0,0,0.35)) &:hover, &:active, &:focus - background-color: #b2b2b2 + background-color: rgba(255,255,255,1) i.ofn-i-058-graph color: $clr-brick-bright From 5f409e44768a5a2f67fce281e084c89cad3e3232 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 16:59:14 +1000 Subject: [PATCH 067/205] Tweak label for shopping cart CTA button again --- app/views/shop/products/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 80c934d7e5..16e003c83e 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -12,7 +12,7 @@ %form{action: cart_path} .small-12.medium-4.large-3.columns - %input.button.primary.right.add_to_cart{type: :submit, value: "Shopping cart >", + %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", "ng-disabled" => "Cart.dirty"} %div.pad-top{bindonce: true} @@ -38,6 +38,6 @@ .row .small-12.columns %form{action: cart_path} - %input.button.primary.right.add_to_cart{type: :submit, value: "Shopping cart >", + %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", "ng-disabled" => "Cart.dirty"} From fe4980eadd64958a410d40782467de1f65fd36c6 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 6 Aug 2014 17:07:27 +1000 Subject: [PATCH 068/205] Tweaking styling on popovers --- app/assets/stylesheets/darkswarm/_shop-popovers.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index 5462469660..3412f3ccd3 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -77,7 +77,7 @@ button.graph-button @include box-shadow(none) padding: 0 margin: 0 - @include border-radius(99999rem) + @include border-radius(999rem) display: inline background-color: rgba(255,255,255,0.5) padding: 0.35rem From 7d7bf7b1f2e8c9de82c7aae528ace8a010069641 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 12:06:02 +1000 Subject: [PATCH 069/205] Tweak layout for cart to make it more robust for content and allow for longer product labels --- app/views/shared/menu/_cart.html.haml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index 44047e5261..a61f80bf88 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -14,20 +14,21 @@ %li.product-cart{"ng-repeat" => "line_item in Cart.line_items_present()", "ng-controller" => "LineItemCtrl"} .row - .columns.small-6 + .columns.small-7 %small %strong {{ line_item.variant.name_to_display }} %em {{ line_item.variant.unit_to_display }} - .columns.small-3 - %small - {{line_item.quantity}} - x - {{ line_item.variant.price | currency }} - .columns.small-3.text-right + %small + {{line_item.quantity}} + %i.ofn-i_009-close + {{ line_item.variant.price | currency }} + + .columns.small-2 %small \= - %strong {{ line_item.variant.getPrice() | currency }} + %strong + .right {{ line_item.variant.getPrice() | currency }} %li.total-cart{"ng-show" => "Cart.line_items_present().length > 0"} .row From 68ed327efcfe7929bf02298bb1f240f3d69c13c2 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 12:06:42 +1000 Subject: [PATCH 070/205] Tweaking popover styles for shop price breakdown popovers --- app/assets/stylesheets/darkswarm/_shop-popovers.css.sass | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index 3412f3ccd3..1b511deb6c 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -22,6 +22,7 @@ ordercycle // JS needs to be tweaked to adjust for left alignment - this is dynamic can't rewrite in CSS background-color: #999 color: #1f1f1f + margin-left: -8px @include box-shadow(0 1px 2px 0 rgba(0,0,0,0.7)) .joyride-content-wrapper @@ -35,7 +36,7 @@ ordercycle color: #1f1f1f .joyride-nub.right - top: 40px + top: 38px border-color: #999 !important border-top-color: transparent !important border-right-color: transparent !important @@ -74,13 +75,15 @@ ordercycle button.graph-button + z-index: 9999999 + border: 1px solid transparent @include box-shadow(none) padding: 0 margin: 0 @include border-radius(999rem) display: inline background-color: rgba(255,255,255,0.5) - padding: 0.35rem + padding: 5px &:hover, &:active, &:focus background-color: rgba(255,255,255,1) @@ -95,6 +98,7 @@ button.graph-button button.graph-button.open @include box-shadow(inset 0 1px 1px 0 rgba(0,0,0,0.35)) + border: 1px solid #999 &:hover, &:active, &:focus background-color: rgba(255,255,255,1) From 591c3959f9a9627cd3dde5e9f0e860a533db7e9c Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 12:07:04 +1000 Subject: [PATCH 071/205] Make shopping cart wider by default --- app/assets/stylesheets/darkswarm/shopping-cart.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass index 3a63bb7a2e..72856eba72 100644 --- a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass +++ b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass @@ -12,7 +12,7 @@ display: block right: 10px top: 55px - width: 400px + width: 480px @media screen and (max-width: 640px) width: 96% From 30a14edb0643556dea4e94e3c154580a2a36f7ae Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 7 Aug 2014 14:21:01 +1000 Subject: [PATCH 072/205] Adding 'Total Units' field to supplier report --- .../admin/reports_controller_decorator.rb | 13 ++++++- lib/open_food_network/option_value_namer.rb | 39 +++++++++++-------- .../option_value_namer_spec.rb | 32 +++++++-------- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 377f1dbedf..28174724dd 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -386,12 +386,23 @@ Spree::Admin::ReportsController.class_eval do table_items = @line_items @include_blank = 'All' - header = ["Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] + header = ["Producer", "Product", "Variant", "Amount", "Total Units", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] + + ovn = OpenFoodNetwork::OptionValueNamer.new() columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, proc { |line_items| line_items.first.variant.product.name }, proc { |line_items| line_items.first.variant.full_name }, proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| ovn.name(OpenStruct.new({ + unit_value: ( line_items.map{ |li| li.variant.unit_value.nil? }.any? ? 0 : line_items.sum { |li| li.quantity * li.variant.unit_value } ), + unit_description: line_items.first.variant.unit_description, + product: OpenStruct.new({ + variant_unit: line_items.first.product.variant_unit, + variant_unit_scale: line_items.first.product.variant_unit_scale, + variant_unit_name: line_items.first.product.variant_unit_name + }) + }))}, proc { |line_items| line_items.first.variant.price }, proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, proc { |line_items| "" }, diff --git a/lib/open_food_network/option_value_namer.rb b/lib/open_food_network/option_value_namer.rb index 101fae427c..2c2383906d 100644 --- a/lib/open_food_network/option_value_namer.rb +++ b/lib/open_food_network/option_value_namer.rb @@ -1,27 +1,34 @@ module OpenFoodNetwork - class OptionValueNamer < Struct.new(:variant) - def name - value, unit = self.option_value_value_unit - separator = self.value_scaled? ? '' : ' ' + class OptionValueNamer + def initialize(variant = nil) + @variant = variant + end + + def name(obj = nil) + @variant = obj unless obj.nil? + value, unit = option_value_value_unit + separator = value_scaled? ? '' : ' ' name_fields = [] name_fields << "#{value}#{separator}#{unit}" if value.present? && unit.present? - name_fields << variant.unit_description if variant.unit_description.present? + name_fields << @variant.unit_description if @variant.unit_description.present? name_fields.join ' ' end + private + def value_scaled? - variant.product.variant_unit_scale.present? + @variant.product.variant_unit_scale.present? end def option_value_value_unit - if variant.unit_value.present? - if %w(weight volume).include? variant.product.variant_unit - value, unit_name = self.option_value_value_unit_scaled + if @variant.unit_value.present? + if %w(weight volume).include? @variant.product.variant_unit + value, unit_name = option_value_value_unit_scaled else - value = variant.unit_value - unit_name = variant.product.variant_unit_name + value = @variant.unit_value + unit_name = @variant.product.variant_unit_name unit_name = unit_name.pluralize if value > 1 end @@ -35,9 +42,9 @@ module OpenFoodNetwork end def option_value_value_unit_scaled - unit_scale, unit_name = self.scale_for_unit_value + unit_scale, unit_name = scale_for_unit_value - value = variant.unit_value / unit_scale + value = @variant.unit_value / unit_scale [value, unit_name] end @@ -48,10 +55,10 @@ module OpenFoodNetwork # Find the largest available unit where unit_value comes to >= 1 when expressed in it. # If there is none available where this is true, use the smallest available unit. - unit = units[variant.product.variant_unit].select { |scale, unit_name| - variant.unit_value / scale >= 1 + unit = units[@variant.product.variant_unit].select { |scale, unit_name| + @variant.unit_value / scale >= 1 }.to_a.last - unit = units[variant.product.variant_unit].first if unit.nil? + unit = units[@variant.product.variant_unit].first if unit.nil? unit end diff --git a/spec/lib/open_food_network/option_value_namer_spec.rb b/spec/lib/open_food_network/option_value_namer_spec.rb index b16574ba4c..354fd46369 100644 --- a/spec/lib/open_food_network/option_value_namer_spec.rb +++ b/spec/lib/open_food_network/option_value_namer_spec.rb @@ -4,34 +4,34 @@ module OpenFoodNetwork describe OptionValueNamer do describe "generating option value name" do let(:v) { Spree::Variant.new } - let(:subject) { OptionValueNamer.new v } + let(:subject) { OptionValueNamer.new } it "when description is blank" do v.stub(:unit_description) { nil } subject.stub(:value_scaled?) { true } subject.stub(:option_value_value_unit) { %w(value unit) } - subject.name.should == "valueunit" + subject.name(v).should == "valueunit" end it "when description is present" do v.stub(:unit_description) { 'desc' } subject.stub(:option_value_value_unit) { %w(value unit) } subject.stub(:value_scaled?) { true } - subject.name.should == "valueunit desc" + subject.name(v).should == "valueunit desc" end it "when value is blank and description is present" do v.stub(:unit_description) { 'desc' } subject.stub(:option_value_value_unit) { [nil, nil] } subject.stub(:value_scaled?) { true } - subject.name.should == "desc" + subject.name(v).should == "desc" end it "spaces value and unit when value is unscaled" do v.stub(:unit_description) { nil } subject.stub(:option_value_value_unit) { %w(value unit) } subject.stub(:value_scaled?) { false } - subject.name.should == "value unit" + subject.name(v).should == "value unit" end end @@ -42,7 +42,7 @@ module OpenFoodNetwork v.stub(:product) { p } subject = OptionValueNamer.new v - subject.value_scaled?.should be_true + expect(subject.send(:value_scaled?)).to be_true end it "returns false otherwise" do @@ -51,7 +51,7 @@ module OpenFoodNetwork v.stub(:product) { p } subject = OptionValueNamer.new v - subject.value_scaled?.should be_false + expect(subject.send(:value_scaled?)).to be_false end end @@ -65,7 +65,7 @@ module OpenFoodNetwork v.stub(:unit_value) { 100 } - subject.option_value_value_unit.should == [100, 'g'] + expect(subject.send(:option_value_value_unit)).to eq [100, 'g'] end it "generates values when unit value is non-integer" do @@ -73,7 +73,7 @@ module OpenFoodNetwork v.stub(:product) { p } v.stub(:unit_value) { 123.45 } - subject.option_value_value_unit.should == [123.45, 'g'] + expect(subject.send(:option_value_value_unit)).to eq [123.45, 'g'] end it "returns a value of 1 when unit value equals the scale" do @@ -81,7 +81,7 @@ module OpenFoodNetwork v.stub(:product) { p } v.stub(:unit_value) { 1000.0 } - subject.option_value_value_unit.should == [1, 'kg'] + expect(subject.send(:option_value_value_unit)).to eq [1, 'kg'] end it "generates values for all weight scales" do @@ -89,7 +89,7 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'weight', variant_unit_scale: scale) v.stub(:product) { p } v.stub(:unit_value) { 100 * scale } - subject.option_value_value_unit.should == [100, unit] + expect(subject.send(:option_value_value_unit)).to eq [100, unit] end end @@ -98,7 +98,7 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'volume', variant_unit_scale: scale) v.stub(:product) { p } v.stub(:unit_value) { 100 * scale } - subject.option_value_value_unit.should == [100, unit] + expect(subject.send(:option_value_value_unit)).to eq [100, unit] end end @@ -106,7 +106,7 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'volume', variant_unit_scale: 0.001) v.stub(:product) { p } v.stub(:unit_value) { 0.0001 } - subject.option_value_value_unit.should == [0.1, 'mL'] + expect(subject.send(:option_value_value_unit)).to eq [0.1, 'mL'] end it "generates values for item units" do @@ -114,7 +114,7 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit) v.stub(:product) { p } v.stub(:unit_value) { 100 } - subject.option_value_value_unit.should == [100, unit.pluralize] + expect(subject.send(:option_value_value_unit)).to eq [100, unit.pluralize] end end @@ -122,14 +122,14 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: 'packet') v.stub(:product) { p } v.stub(:unit_value) { 1 } - subject.option_value_value_unit.should == [1, 'packet'] + expect(subject.send(:option_value_value_unit)).to eq [1, 'packet'] end it "returns [nil, nil] when unit value is not set" do p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: 'foo') v.stub(:product) { p } v.stub(:unit_value) { nil } - subject.option_value_value_unit.should == [nil, nil] + expect(subject.send(:option_value_value_unit)).to eq [nil, nil] end end end From d1c9980f3fbb2483357acc6dfe6586851329bd74 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 7 Aug 2014 14:47:49 +1000 Subject: [PATCH 073/205] Checkout include distribution fees in cart total --- app/helpers/checkout_helper.rb | 17 +++++++++++++++-- app/views/checkout/_summary.html.haml | 26 ++++++++++++++------------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index 5faccb781d..171a672cda 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -1,18 +1,31 @@ module CheckoutHelper def checkout_adjustments_for_summary(order, opts={}) adjustments = order.adjustments.eligible + exclude = opts[:exclude] || {} # Remove empty tax adjustments and (optionally) shipping fees adjustments.reject! { |a| a.originator_type == 'Spree::TaxRate' && a.amount == 0 } - adjustments.reject! { |a| a.originator_type == 'Spree::ShippingMethod' } if opts[:exclude_shipping] + adjustments.reject! { |a| a.originator_type == 'Spree::ShippingMethod' } if exclude.include? :shipping enterprise_fee_adjustments = adjustments.select { |a| a.originator_type == 'EnterpriseFee' } adjustments.reject! { |a| a.originator_type == 'EnterpriseFee' } - adjustments << Spree::Adjustment.new(label: 'Distribution', amount: enterprise_fee_adjustments.sum(&:amount)) + unless exclude.include? :distribution + adjustments << Spree::Adjustment.new(label: 'Distribution', amount: enterprise_fee_adjustments.sum(&:amount)) + end adjustments end + def checkout_adjustments_total(order) + adjustments = checkout_adjustments_for_summary(order, exclude: [:shipping]) + adjustments.sum &:display_amount + end + + def checkout_cart_total_with_adjustments(order) + current_order.display_item_total.money.to_f + checkout_adjustments_total(current_order).money.to_f + end + + def validated_input(name, path, args = {}) attributes = { required: true, diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index bc0a0e307f..0e03af7413 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -5,23 +5,25 @@ %table %tr %th Cart total - %td.text-right= current_order.display_item_total + %td.cart-total.text-right= number_to_currency checkout_cart_total_with_adjustments(current_order) + + - checkout_adjustments_for_summary(current_order, exclude: [:shipping, :distribution]).each do |adjustment| + %tr + %th= adjustment.label + %td.text-right= adjustment.display_amount.to_html - / - checkout_adjustments_for_summary(current_order, exclude_shipping: true).each do |adjustment| - / %tr - / %th= adjustment.label - / %td.text-right= adjustment.display_amount.to_html %tr %th Shipping - %td.text-right {{ Checkout.shippingPrice() | currency }} + %td.shipping.text-right {{ Checkout.shippingPrice() | currency }} + %tr %th Total - %td.text-right {{ Checkout.cartTotal() | currency }} - / - if current_order.price_adjustment_totals.present? - / - current_order.price_adjustment_totals.each do |label, total| - / %tr - / %th= label - / %td= total + %td.total.text-right {{ Checkout.cartTotal() | currency }} + - if current_order.price_adjustment_totals.present? + - current_order.price_adjustment_totals.each do |label, total| + %tr + %th= label + %td= total //= f.submit "Purchase", class: "button", "ng-disabled" => "checkout.$invalid", "ofn-focus" => "accordion['payment']" %a.button.secondary{href: cart_url} From 769e8410ccff49fb336e48e265c5d942394f13df Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 7 Aug 2014 14:51:48 +1000 Subject: [PATCH 074/205] Fix checkout total display --- .../services/shipping_methods.js.coffee | 1 + .../consumer/shopping/checkout_spec.rb | 23 ++++++++++++++----- .../services/shipping_methods_spec.js.coffee | 14 +++++++++++ spec/support/request/shop_workflow.rb | 10 +++++++- 4 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 spec/javascripts/unit/darkswarm/services/shipping_methods_spec.js.coffee diff --git a/app/assets/javascripts/darkswarm/services/shipping_methods.js.coffee b/app/assets/javascripts/darkswarm/services/shipping_methods.js.coffee index 3980aa5fed..6d060179b3 100644 --- a/app/assets/javascripts/darkswarm/services/shipping_methods.js.coffee +++ b/app/assets/javascripts/darkswarm/services/shipping_methods.js.coffee @@ -4,5 +4,6 @@ Darkswarm.factory "ShippingMethods", (shippingMethods)-> shipping_methods_by_id: {} constructor: -> for method in @shipping_methods + method.price = parseFloat(method.price) @shipping_methods_by_id[method.id] = method diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 9c5d56e8aa..e3e2c9c4ad 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -10,27 +10,29 @@ feature "As a consumer I want to check out my cart", js: true do let(:distributor) { create(:distributor_enterprise) } let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } + let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } + let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23) } let(:product) { create(:simple_product, supplier: supplier) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } before do + add_enterprise_fee enterprise_fee set_order order add_product_to_cart end - it "shows the current distributor oncheckout" do + it "shows the current distributor on checkout" do visit checkout_path page.should have_content distributor.name end describe "with shipping methods" do let(:sm1) { create(:shipping_method, require_ship_address: true, name: "Frogs", description: "yellow") } - let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue") } + let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 4.56)) } before do - distributor.shipping_methods << sm1 - distributor.shipping_methods << sm2 + distributor.shipping_methods << sm1 + distributor.shipping_methods << sm2 end context "on the checkout page" do @@ -39,6 +41,15 @@ feature "As a consumer I want to check out my cart", js: true do checkout_as_guest end + it "shows a breakdown of the order price" do + toggle_shipping + choose sm2.name + + page.should have_selector 'orderdetails .cart-total', text: "$11.23" + page.should have_selector 'orderdetails .shipping', text: "$4.56" + page.should have_selector 'orderdetails .total', text: "$15.79" + end + it "shows all shipping methods, but doesn't show ship address when not needed" do toggle_shipping page.should have_content "Frogs" @@ -157,7 +168,7 @@ feature "As a consumer I want to check out my cart", js: true do # Order should have a payment with the correct amount o = Spree::Order.complete.first - o.payments.first.amount.should == 10 + o.payments.first.amount.should == 11.23 end it "shows the payment processing failed message when submitted with an invalid credit card" do diff --git a/spec/javascripts/unit/darkswarm/services/shipping_methods_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/shipping_methods_spec.js.coffee new file mode 100644 index 0000000000..ebfba165f6 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/shipping_methods_spec.js.coffee @@ -0,0 +1,14 @@ +describe "Shipping method service", -> + ShippingMethods = null + shippingMethods = [ + {id: 1, price: "1.2"} + ] + + beforeEach -> + module 'Darkswarm' + angular.module('Darkswarm').value('shippingMethods', shippingMethods) + inject ($injector)-> + ShippingMethods = $injector.get("ShippingMethods") + + it "converts price to float", -> + expect(ShippingMethods.shipping_methods[0].price).toEqual 1.2 diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index e8fd6e3b17..6c47d6f706 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -7,13 +7,21 @@ module ShopWorkflow have_selector ".price", text: price end + def add_enterprise_fee(enterprise_fee) + order_cycle.exchanges.outgoing.first.enterprise_fees << enterprise_fee + end + def set_order(order) ApplicationController.any_instance.stub(:session).and_return({order_id: order.id, access_token: order.token}) end def add_product_to_cart create(:line_item, variant: product.master, order: order) - order.reload.save! # Recalculate totals + order.reload + + # Recalculate totals + order.save! + order.update_distribution_charge! end def toggle_accordion(name) From c6a350af7552b56e7c285ef9fd035296a226fe95 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 14:15:19 +1000 Subject: [PATCH 075/205] Better column layout for responsive --- .../javascripts/templates/partials/hub_details.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/templates/partials/hub_details.html.haml b/app/assets/javascripts/templates/partials/hub_details.html.haml index 256494bb4a..7273c5008a 100644 --- a/app/assets/javascripts/templates/partials/hub_details.html.haml +++ b/app/assets/javascripts/templates/partials/hub_details.html.haml @@ -1,10 +1,10 @@ .row.pad-top{bindonce: true} .cta-container.small-12.columns .row - .small-12.large-6.columns - %label{"active-table-hub-link" => "enterprise", change: "Change hub to", shop: "Shop now at"} - .small-12.large-6.columns.right - .right{"bo-if" => "enterprise.pickup || enterprise.delivery"} + .small-4.columns + %label{"active-table-hub-link" => "enterprise", change: "Change hub to:", shop: "Shop now at:"} + .small-8.columns.right + %label.right{"bo-if" => "enterprise.pickup || enterprise.delivery"} Delivery options: %span{"bo-if" => "enterprise.pickup"} %i.ofn-i_038-takeaway From cd774a3a2b8a8009307101b4886cf82e6b6ce45f Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 14:51:32 +1000 Subject: [PATCH 076/205] Fix small device width issue on google map embedding --- app/assets/stylesheets/darkswarm/map.css.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/darkswarm/map.css.sass b/app/assets/stylesheets/darkswarm/map.css.sass index 78d350d6be..53e8fb5768 100644 --- a/app/assets/stylesheets/darkswarm/map.css.sass +++ b/app/assets/stylesheets/darkswarm/map.css.sass @@ -8,6 +8,7 @@ map, .angular-google-map-container, google-map, .angular-google-map display: block height: 100% + width: 100% img // https://github.com/zurb/foundation/issues/112 max-width: none From febe66b7b0ba4f812ca91ed716e5d669145c5ff6 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 15:21:53 +1000 Subject: [PATCH 077/205] Styling for modals - making media queries look at container height rather than width --- .../darkswarm/modal-enterprises.css.sass | 11 +++++-- .../stylesheets/darkswarm/modals.css.sass | 30 ++++++++++++------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass index 7991969a7d..11a8b62c5c 100644 --- a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass +++ b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass @@ -60,19 +60,26 @@ // CALL TO ACTION - hub click throughs .cta-container - background: url("/assets/gray_jean.png") repeat + background-color: #ececec padding-top: 0.75rem label text-transform: uppercase font-size: 0.875rem - margin-bottom: 0.5rem + margin-bottom: 0 + 5rem color: $dark-grey + label.right + color: $disabled-dark + span + text-transform: capitalize + .button.secondary background-color: #999 .button.hub margin-right: 1rem + margin-top: 0.25rem margin-bottom: 1rem padding-left: 1rem padding-right: 1rem diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index a4a896942b..fbb4f6570d 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -4,24 +4,34 @@ dialog, .reveal-modal border: none outline: none - padding: 1rem - // TO DO: look at bigger issue scrolling on mobile device + padding: 1rem overflow-y: scroll - @media only screen and (min-width: 40.063em) - max-height: 580px - @media all and (max-width: 768px) + + // Sets up max heights based on device height + @media all and (min-height: 1025px) + max-height: 800px + + @media all and (min-height: 700px) and (max-height: 1024px) + max-height: 600px + + @media all and (min-height: 600px) and (max-height: 699px) + max-height: 560px + + @media all and (min-height: 481px) and (max-height: 599px) max-height: 440px - @media all and (max-width: 640px) - max-height: 400px - @media all and (max-width: 640px) - max-height: inherit + + @media only screen and (min-height: 480px) + max-height: 440px + + @media handheld and (min-height: 480px) + max-height: 100% overflow-y: scroll + .reveal-modal-bg background-color: rgba(0,0,0,0.65) dialog .close-reveal-modal, .reveal-modal .close-reveal-modal - top: 0.45rem right: 0.4rem background-color: rgba(235,235,235,0.85) text-shadow: none From 28a04c5e849800cea189de52ea5aa6bd8489f9e4 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 15:31:05 +1000 Subject: [PATCH 078/205] Fix media query --- app/assets/stylesheets/darkswarm/modals.css.sass | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index fbb4f6570d..3080747914 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -20,10 +20,10 @@ dialog, .reveal-modal @media all and (min-height: 481px) and (max-height: 599px) max-height: 440px - @media only screen and (min-height: 480px) + @media only screen and (max-height: 480px) max-height: 440px - @media handheld and (min-height: 480px) + @media handheld and (max-height: 480px) max-height: 100% overflow-y: scroll From be9acf526d688d7d07f91c25a45a14eb4fd73cfa Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 15:39:39 +1000 Subject: [PATCH 079/205] Change modal height to perpercentage of container height --- app/assets/stylesheets/darkswarm/modals.css.sass | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index 3080747914..f489e5a75f 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -9,19 +9,19 @@ dialog, .reveal-modal // Sets up max heights based on device height @media all and (min-height: 1025px) - max-height: 800px + max-height: 80% @media all and (min-height: 700px) and (max-height: 1024px) - max-height: 600px + max-height: 70% @media all and (min-height: 600px) and (max-height: 699px) - max-height: 560px + max-height: 60% @media all and (min-height: 481px) and (max-height: 599px) - max-height: 440px + max-height: 60% @media only screen and (max-height: 480px) - max-height: 440px + max-height: 60% @media handheld and (max-height: 480px) max-height: 100% From bda9030ae47effa8daf2a2aa20af8a249612db15 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 16:17:15 +1000 Subject: [PATCH 080/205] Add a word wrap class to force long strings to wrap --- app/assets/stylesheets/darkswarm/typography.css.sass | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/typography.css.sass b/app/assets/stylesheets/darkswarm/typography.css.sass index 254a277055..b49ce867c1 100644 --- a/app/assets/stylesheets/darkswarm/typography.css.sass +++ b/app/assets/stylesheets/darkswarm/typography.css.sass @@ -34,6 +34,10 @@ small, .small margin-bottom: 0.5rem &, & * font-size: 0.875rem + +.word-wrap + word-wrap: break-word + .light color: #999 display: inline From c2540234f1164bd152255ccb3e72510adb420c68 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 16:18:02 +1000 Subject: [PATCH 081/205] Add class to force word wrap --- app/assets/javascripts/templates/partials/contact.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/templates/partials/contact.html.haml b/app/assets/javascripts/templates/partials/contact.html.haml index 0f7ffb033d..78828984c4 100644 --- a/app/assets/javascripts/templates/partials/contact.html.haml +++ b/app/assets/javascripts/templates/partials/contact.html.haml @@ -4,11 +4,11 @@ %p{"ng-if" => "enterprise.phone"} {{ enterprise.phone }} - %p{"ng-if" => "enterprise.email"} + %p.word-wrap{"ng-if" => "enterprise.email"} %a{"ng-href" => "{{enterprise.email | stripUrl}}", target: "_blank", mailto: true} %span.email {{ enterprise.email | stripUrl }} - %p{"ng-if" => "enterprise.website"} + %p.word-wrap{"ng-if" => "enterprise.website"} %a{"ng-href" => "http://{{enterprise.website | stripUrl}}", target: "_blank" } {{ enterprise.website | stripUrl }} From fb7547ae2f7a547e2d8fcc306c19897ff255d706 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 16:23:19 +1000 Subject: [PATCH 082/205] Add a little padding on modals --- app/assets/stylesheets/darkswarm/modal-enterprises.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass index 11a8b62c5c..837fd8e08a 100644 --- a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass +++ b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass @@ -121,4 +121,4 @@ border-bottom: 1px solid $disabled-dark margin-top: 0.75rem margin-bottom: 0.5rem - + From c2690d0c203b88999a0e6727a443c60a70e8f590 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 16:23:47 +1000 Subject: [PATCH 083/205] Tweak responsive queries --- app/assets/stylesheets/darkswarm/modals.css.sass | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index f489e5a75f..0681dd73b4 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -20,11 +20,10 @@ dialog, .reveal-modal @media all and (min-height: 481px) and (max-height: 599px) max-height: 60% - @media only screen and (max-height: 480px) + @media only screen and (max-height: 480px) and (min-width: 641px) max-height: 60% - @media handheld and (max-height: 480px) - max-height: 100% + @media all and (max-height: 480px) overflow-y: scroll From f8c71ced4521492a0eb8bdf0d04e0f18f84a654c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 7 Aug 2014 15:08:07 +1000 Subject: [PATCH 084/205] Fix display of state_name, after attribute was renamed in serializer --- .../javascripts/templates/partials/enterprise_header.html.haml | 2 +- app/assets/javascripts/templates/partials/hub_actions.html.haml | 2 +- app/assets/javascripts/templates/partials/hub_details.html.haml | 2 +- spec/features/consumer/groups_spec.rb | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/templates/partials/enterprise_header.html.haml b/app/assets/javascripts/templates/partials/enterprise_header.html.haml index 7b1fc74a03..cb41670831 100644 --- a/app/assets/javascripts/templates/partials/enterprise_header.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_header.html.haml @@ -1,7 +1,7 @@ .highlight .highlight-top %p.right - {{ [enterprise.address.city, enterprise.address.state] | printArray}} + {{ [enterprise.address.city, enterprise.address.state_name] | printArray}} %h3{"ng-if" => "enterprise.is_distributor"} %a{"bo-href" => "enterprise.path", "ofn-empties-cart" => "enterprise", bindonce: true} %i.ofn-i_040-hub diff --git a/app/assets/javascripts/templates/partials/hub_actions.html.haml b/app/assets/javascripts/templates/partials/hub_actions.html.haml index 2ed8bf3f65..54b8ee98b1 100644 --- a/app/assets/javascripts/templates/partials/hub_actions.html.haml +++ b/app/assets/javascripts/templates/partials/hub_actions.html.haml @@ -11,6 +11,6 @@ %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} {{hub.name}} - .button-address {{ hub.address.city }} , {{hub.address.state}} + .button-address {{ hub.address.city }} , {{hub.address.state_name}} %i.ofn-i_007-caret-right diff --git a/app/assets/javascripts/templates/partials/hub_details.html.haml b/app/assets/javascripts/templates/partials/hub_details.html.haml index 7273c5008a..8de1039f10 100644 --- a/app/assets/javascripts/templates/partials/hub_details.html.haml +++ b/app/assets/javascripts/templates/partials/hub_details.html.haml @@ -20,5 +20,5 @@ %i.ofn-i_033-open-sign{"bo-if" => "enterprise.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!enterprise.active"} {{enterprise.name}} - .button-address {{ enterprise.address.city }} , {{enterprise.address.state}} + .button-address {{ enterprise.address.city }} , {{enterprise.address.state_name}} %i.ofn-i_007-caret-right diff --git a/spec/features/consumer/groups_spec.rb b/spec/features/consumer/groups_spec.rb index 7baa5a2807..b2db10d58c 100644 --- a/spec/features/consumer/groups_spec.rb +++ b/spec/features/consumer/groups_spec.rb @@ -17,5 +17,6 @@ feature 'Groups', js: true do page.should have_content enterprise.name open_enterprise_modal enterprise modal_should_be_open_for enterprise + page.should have_content "Herndon, Vic" end end From e28e86a82cc71bc10fb6347254dca6c99c70df81 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 7 Aug 2014 15:11:25 +1000 Subject: [PATCH 085/205] Remove link from cart to old product page --- app/views/spree/orders/_cart_item_description.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/spree/orders/_cart_item_description.html.haml b/app/views/spree/orders/_cart_item_description.html.haml index 9a516168bc..8c99781d5c 100644 --- a/app/views/spree/orders/_cart_item_description.html.haml +++ b/app/views/spree/orders/_cart_item_description.html.haml @@ -1,7 +1,7 @@ %td{'data-hook' => "cart_item_description"} - %h4= link_to variant.product.name, product_path(variant.product) + %h4= variant.product.name = variant.options_text - if @order.insufficient_stock_lines.include? line_item %span.out-of-stock = variant.in_stock? ? t(:insufficient_stock, :on_hand => variant.on_hand) : t(:out_of_stock) - %br/ \ No newline at end of file + %br/ From 4f0d55a859d0fe1fdd8649f7a8bf41c904550bbb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 7 Aug 2014 16:02:30 +1000 Subject: [PATCH 086/205] Link error page images absolutely, fixes image not found on eg. /page/not/found --- public/404.html | 2 +- public/422.html | 2 +- public/500.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/404.html b/public/404.html index 77830da9f7..b650949066 100644 --- a/public/404.html +++ b/public/404.html @@ -28,7 +28,7 @@
- +

It seems the page you're looking for is in a grump.

Return home

diff --git a/public/422.html b/public/422.html index b222475557..c6a93d7ab6 100644 --- a/public/422.html +++ b/public/422.html @@ -28,7 +28,7 @@
- +

The change you wanted was rejected. Maybe you tried to change something you don't have access to.

Return home

diff --git a/public/500.html b/public/500.html index 30379bc44d..5c7fa05f07 100644 --- a/public/500.html +++ b/public/500.html @@ -28,7 +28,7 @@
- +

We're sorry, but something went wrong.
Try refreshing the page, or

Return home

From 116882f0a99e6c4645cb24d4402c88f80bafc426 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 7 Aug 2014 16:45:22 +1000 Subject: [PATCH 087/205] More responsive tweaks --- app/assets/stylesheets/darkswarm/_shop-popovers.css.sass | 7 ++++++- app/assets/stylesheets/darkswarm/shop.css.sass | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index 1b511deb6c..6fa8a72c1b 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -83,7 +83,7 @@ button.graph-button @include border-radius(999rem) display: inline background-color: rgba(255,255,255,0.5) - padding: 5px + padding: 5px &:hover, &:active, &:focus background-color: rgba(255,255,255,1) @@ -96,6 +96,11 @@ button.graph-button padding: 0 font-size: 1rem + @media all and (max-width: 640px) + padding: 3px + i.ofn-i-058-graph + font-size: 0.75rem + button.graph-button.open @include box-shadow(inset 0 1px 1px 0 rgba(0,0,0,0.35)) border: 1px solid #999 diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index a2df3706c7..083defca77 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -60,6 +60,8 @@ i font-size: 0.75em padding-right: 0.9375rem + @media all and (max-width: 640px) + padding-right: 0.25rem i.ofn-i_056-bulk, i.ofn-i_036-producers font-size: 1rem From 7476860b47a8e726b219c487740752eb45aa16a6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 7 Aug 2014 17:20:38 +1000 Subject: [PATCH 088/205] Comment out intermittently failing spec - we intend to remove product distributions soon anyway --- spec/models/product_distribution_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/models/product_distribution_spec.rb b/spec/models/product_distribution_spec.rb index 44ebb3cfd3..7e1a6933bc 100644 --- a/spec/models/product_distribution_spec.rb +++ b/spec/models/product_distribution_spec.rb @@ -25,6 +25,8 @@ describe ProductDistribution do describe "adjusting orders" do context "integration" do it "creates an adjustment for product distributions" do + pending "Intermittently failing spec - we intend to remove product distributions soon" + # Given an order distributor = create(:distributor_enterprise) order = create(:order, distributor: distributor) From c30d7fe72ae08629728b2e38c4cc3b7a28552da2 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 8 Aug 2014 09:56:55 +1000 Subject: [PATCH 089/205] Fix broken JS specs --- .../javascripts/unit/darkswarm/services/product_spec.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee index 237cbcd369..d7c5a20b91 100644 --- a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee @@ -14,6 +14,7 @@ describe 'Products service', -> supplier: id: 9 price: 11 + master: {} variants: [] currentOrder = line_items: [] @@ -39,7 +40,7 @@ describe 'Products service', -> it "dereferences suppliers", -> Enterprises.enterprises_by_id = {id: 9, name: "test"} - $httpBackend.expectGET("/shop/products").respond([{supplier : {id: 9}}]) + $httpBackend.expectGET("/shop/products").respond([{supplier : {id: 9}, master: {}}]) $httpBackend.flush() expect(Products.products[0].supplier).toBe Enterprises.enterprises_by_id["9"] From f3e43ebd29f331133daf632889b16b2a142bda4d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 8 Aug 2014 11:10:51 +1000 Subject: [PATCH 090/205] Use .location for testability --- .../controllers/authentication/login_controller.js.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee index c24f7a70f2..144cf0dfef 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee @@ -1,13 +1,13 @@ -Darkswarm.controller "LoginCtrl", ($scope, $http, AuthenticationService, Redirections, Loading) -> +Darkswarm.controller "LoginCtrl", ($scope, $http, $window, AuthenticationService, Redirections, Loading) -> $scope.path = "/login" $scope.submit = -> Loading.message = "Hold on a moment, we're logging you in" $http.post("/user/spree_user/sign_in", {spree_user: $scope.spree_user}).success (data)-> if Redirections.after_login - location.href = location.origin + Redirections.after_login + $window.location.href = $window.location.origin + Redirections.after_login else - location.href = location.origin + location.pathname # Strips out hash fragments + $window.location.href = $window.location.origin + $window.location.pathname # Strips out hash fragments .error (data) -> Loading.clear() $scope.errors = data.message From 16847025dd3ec454873794955fb4633a8eb8ee79 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 8 Aug 2014 11:14:50 +1000 Subject: [PATCH 091/205] Fix bug: local storage was not correctly keyed to user id --- .../checkout/checkout_controller.js.coffee | 2 +- .../consumer/shopping/checkout_auth_spec.rb | 16 +++++++++++++++- .../checkout/checkout_controller_spec.js.coffee | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee index a5966e503b..e77712eae6 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee @@ -3,7 +3,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, storage, Checkout, CurrentUser, Cu # Bind to local storage $scope.fieldsToBind = ["bill_address", "email", "payment_method_id", "shipping_method_id", "ship_address"] - prefix = "order_#{Checkout.order.id}#{Checkout.order.user_id}#{CurrentHub.hub.id}" + prefix = "order_#{Checkout.order.id}#{CurrentUser?.id}#{CurrentHub.hub.id}" for field in $scope.fieldsToBind storage.bind $scope, "Checkout.order.#{field}", diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index 81ee44fdcd..2e756879a9 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -12,7 +12,8 @@ feature "As a consumer I want to check out my cart", js: true do let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } let(:product) { create(:simple_product, supplier: supplier) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } - let(:user) { create_enterprise_user } + let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } + let(:user) { create(:user, bill_address: address, ship_address: address) } after { Warden.test_reset! } before do @@ -38,6 +39,19 @@ feature "As a consumer I want to check out my cart", js: true do page.should have_login_modal end + it "populates user details once logged in" do + visit checkout_path + within("section[role='main']") { click_button "Log in" } + page.should have_login_modal + fill_in "Email", with: user.email + fill_in "Password", with: user.password + within(".login-modal") { click_button 'Log in' } + toggle_details + + page.should have_field 'First Name', with: 'Foo' + page.should have_field 'Last Name', with: 'Bar' + end + it "allows user to checkout as guest" do visit checkout_path checkout_as_guest diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee index ceebc55eab..d56a7495ab 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee @@ -46,7 +46,7 @@ describe "CheckoutCtrl", -> describe "Local storage", -> it "binds to localStorage when given a scope", -> - prefix = "order_#{scope.order.id}#{scope.order.user_id}#{CurrentHubMock.hub.id}" + prefix = "order_#{scope.order.id}#{CurrentUser?.id}#{CurrentHubMock.hub.id}" field = scope.fieldsToBind[0] expect(storage.bind).toHaveBeenCalledWith(scope, "Checkout.order.#{field}", {storeName: "#{prefix}_#{field}"}) expect(storage.bind).toHaveBeenCalledWith(scope, "Checkout.ship_address_same_as_billing", {storeName: "#{prefix}_sameasbilling", defaultValue: true}) From a14c2dbb042b6b7265223af502f2355c93c0f9ca Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 9 Jul 2014 17:06:42 +1000 Subject: [PATCH 092/205] Decouple bpe specs from update process --- .../admin/bulk_product_update_spec.rb | 131 +++++++++--------- 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 79cff455a7..ccd0ef479d 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -277,27 +277,23 @@ feature %q{ # When I fill out variant details and hit update fill_in "variant_display_name", with: "Case of 12 Bottles" - fill_in "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)" + fill_in "variant_unit_value_with_description", with: "3 (12x250 mL bottles)" fill_in "variant_display_as", with: "Case" fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + updated_variant = Spree::Variant.where(deleted_at: nil).last + updated_variant.display_name.should == "Case of 12 Bottles" + updated_variant.unit_value.should == 3000 + updated_variant.unit_description.should == "(12x250 mL bottles)" + updated_variant.display_as.should == "Case" + updated_variant.price.should == 4.0 + updated_variant.on_hand.should == 10 + # Then I should see edit buttons for the new variant page.should have_selector "a.edit-variant", visible: true - - # And the variants should be saved - visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" - first("a.view-variants").trigger('click') - - page.should have_field "variant_display_name", with: "Case of 12 Bottles" - page.should have_field "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)" - page.should have_field "variant_display_as", with: "Case" - page.should have_field "variant_price", with: "4.0" - page.should have_field "variant_on_hand", with: "10" end @@ -334,15 +330,15 @@ feature %q{ click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" - visit '/admin/products/bulk_edit' - - page.should have_field "product_name", with: "Big Bag Of Potatoes" - page.should have_select "supplier", selected: s2.name - page.should have_field "available_on", with: (Date.today-3).strftime("%F %T"), visible: false - page.should have_field "price", with: "20.0" - page.should have_select "variant_unit_with_scale", selected: "Weight (kg)" - page.should have_field "on_hand", with: "18" - page.should have_field "display_as", with: "Big Bag" + p.reload + p.name.should == "Big Bag Of Potatoes" + p.supplier.should == s2 + p.variant_unit.should == "weight" + p.variant_unit_scale.should == 1000 # Kg + p.available_on.should == 3.days.ago.beginning_of_day + p.master.display_as.should == "Big Bag" + p.price.should == 20.0 + p.on_hand.should == 18 end scenario "updating a product with a variant unit of 'items'" do @@ -360,10 +356,10 @@ feature %q{ click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" - visit '/admin/products/bulk_edit' - - page.should have_select "variant_unit_with_scale", selected: "Items" - page.should have_field "variant_unit_name", with: "loaf" + p.reload + p.variant_unit.should == "items" + p.variant_unit_scale.should be_nil + p.variant_unit_name.should == "loaf" end scenario "setting a variant unit on a product that has none" do @@ -383,11 +379,12 @@ feature %q{ click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" - visit '/admin/products/bulk_edit' - first("a.view-variants").trigger('click') - - page.should have_select "variant_unit_with_scale", selected: "Weight (kg)" - page.should have_field "variant_unit_value_with_description", with: "123 abc" + p.reload + p.variant_unit.should == "weight" + p.variant_unit_scale.should == 1000 # Kg + v.reload + v.unit_value.should == 123000 # 123 kg in g + v.unit_description.should == "abc" end describe "setting the master unit value for a product without variants" do @@ -407,11 +404,6 @@ feature %q{ click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" - visit '/admin/products/bulk_edit' - - page.should have_select "variant_unit_with_scale", selected: "Weight (kg)" - page.should have_field "master_unit_value_with_description", with: "123 abc" - p.reload p.variant_unit.should == 'weight' p.variant_unit_scale.should == 1000 @@ -450,22 +442,21 @@ feature %q{ page.should have_field "variant_on_hand", with: "9" page.should have_selector "span[name='on_hand']", text: "9" + select "Volume (L)", from: "variant_unit_with_scale" fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" - fill_in "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)" + fill_in "variant_unit_value_with_description", with: "2 (8x250 mL bottles)" page.should have_selector "span[name='on_hand']", text: "10" click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" - visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" - first("a.view-variants").trigger('click') - - page.should have_field "variant_price", with: "4.0" - page.should have_field "variant_on_hand", with: "10" - page.should have_field "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)" + v.reload + v.price.should == 4.0 + v.on_hand.should == 10 + v.unit_value.should == 2 # 2L in L + v.unit_description.should == "(8x250 mL bottles)" end scenario "updating delegated attributes of variants in isolation" do @@ -485,11 +476,8 @@ feature %q{ click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" - visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" - first("a.view-variants").trigger('click') - - page.should have_field "variant_price", with: "10.0" + v.reload + v.price.should == 10.0 end scenario "updating a product mutiple times without refresh" do @@ -504,20 +492,26 @@ feature %q{ click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" + p.reload + p.name.should == "new name 1" fill_in "product_name", with: "new name 2" click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" + p.reload + p.name.should == "new name 2" fill_in "product_name", with: "original name" click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" + p.reload + p.name.should == "original name" end scenario "updating a product after cloning a product" do - FactoryGirl.create(:product, :name => "product 1") + p = FactoryGirl.create(:product, :name => "product 1") login_to_admin_section visit '/admin/products/bulk_edit' @@ -528,12 +522,13 @@ feature %q{ click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" + p.reload + p.name.should == "new product name" end scenario "updating when no changes have been made" do Capybara.using_wait_time(2) do FactoryGirl.create(:product, :name => "product 1") - FactoryGirl.create(:product, :name => "product 2") login_to_admin_section visit '/admin/products/bulk_edit' @@ -562,18 +557,24 @@ feature %q{ click_on 'Update' page.find("span#update-status-message").should have_content "Update complete" + p1.reload + p1.name.should == "new product1" end scenario "updating a product when there are more products than the default API page size" do - 26.times { FactoryGirl.create(:simple_product) } + p = FactoryGirl.create(:simple_product) + 25.times { FactoryGirl.create(:simple_product) } login_to_admin_section visit '/admin/products/bulk_edit' - field = page.all("table#listing_products input[name='product_name']").first - field.set "new name" + within "tr#p_#{p.id}" do + fill_in 'product_name', with: "new name" + end click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" + p.reload + p.name.should == "new name" end describe "using action buttons" do @@ -937,23 +938,25 @@ feature %q{ page.should have_field "on_hand", with: "6" fill_in "product_name", with: "Big Bag Of Potatoes" - select s2.name, from: 'supplier' + select(s2.name, :from => 'supplier') fill_in "available_on", with: (Date.today-3).strftime("%F %T") fill_in "price", with: "20" + select "Weight (kg)", from: "variant_unit_with_scale" fill_in "on_hand", with: "18" + fill_in "display_as", with: "Big Bag" click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" - visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click - - page.should have_field "product_name", with: "Big Bag Of Potatoes" - page.should have_select "supplier", selected: s2.name - page.should have_field "available_on", with: (Date.today-3).strftime("%F %T") - page.should have_field "price", with: "20.0" - page.should have_field "on_hand", with: "18" + p.reload + p.name.should == "Big Bag Of Potatoes" + p.supplier.should == s2 + p.variant_unit.should == "weight" + p.variant_unit_scale.should == 1000 # Kg + p.available_on.should == 3.days.ago.beginning_of_day + p.master.display_as.should == "Big Bag" + p.price.should == 20.0 + p.on_hand.should == 18 end end end From e02a7425975e4de396db8bd70727da169e701a5c Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 24 Jul 2014 18:01:57 +1000 Subject: [PATCH 093/205] WIP: Removing requirment for refresh of products on BPE --- .../admin/bulk_product_update.js.coffee | 83 +++--------- .../unit/bulk_product_update_spec.js.coffee | 119 +++++------------- 2 files changed, 47 insertions(+), 155 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 8fc088fc97..e0d67d1747 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -42,6 +42,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.setPage = (page) -> $scope.currentPage = page $scope.minPage = -> Math.max(1,Math.min($scope.totalPages()-4,$scope.currentPage-2)) $scope.maxPage = -> Math.min($scope.totalPages(),Math.max(5,$scope.currentPage+2)) + $scope.productsWithUnsavedVariants = [] $scope.$watch -> $scope.totalPages() @@ -108,13 +109,14 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ else null - if product.variants - for variant in product.variants - $scope.loadVariantVariantUnit product, variant - $scope.loadVariantVariantUnit product, product.master if product.master + $scope.loadVariantUnitValues product if product.variants + $scope.loadVariantUnitValue product, product.master if product.master + $scope.loadVariantUnitValues = (product) -> + for variant in product.variants + $scope.loadVariantUnitValue product, variant - $scope.loadVariantVariantUnit = (product, variant) -> + $scope.loadVariantUnitValue = (product, variant) -> unit_value = $scope.variantUnitValue product, variant unit_value = if unit_value? then unit_value else '' variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim() @@ -192,6 +194,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ display_name: null on_hand: null price: null + $scope.productsWithUnsavedVariants.push product $scope.displayProperties[product.id].showVariants = true @@ -200,6 +203,11 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.variantIdCounter -= 1 $scope.variantIdCounter + $scope.updateVariantLists = (server_products) -> + for product in $scope.productsWithUnsavedVariants + server_product = $scope.findProduct(product.id, server_products) + product.variants = server_product.variants + $scope.loadVariantUnitValues product $scope.deleteProduct = (product) -> if confirm("Are you sure?") @@ -281,20 +289,9 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ products: productsToSubmit filters: $scope.currentFilters ).success((data) -> - # TODO: remove this check altogether, need to write controller tests if we want to test this behaviour properly - # Note: Rob implemented subset(), which is a simpler alternative to productsWithoutDerivedAttributes(). However, it - # conflicted with some changes I made before merging my work, so for now I've reverted to the old way of - # doing things. TODO: Review together and decide on strategy here. -- Rohan, 14-1-2014 - #if subset($scope.productsWithoutDerivedAttributes(), data) - if $scope.productListsMatch $scope.products, data - $scope.resetProducts data - $timeout -> $scope.displaySuccess() - else - # console.log angular.toJson($scope.productsWithoutDerivedAttributes($scope.products)) - # console.log "---" - # console.log angular.toJson($scope.productsWithoutDerivedAttributes(data)) - # console.log "---" - $scope.displayFailure "Product lists do not match." + DirtyProducts.clear() + #$scope.updateVariantLists(data) + $timeout -> $scope.displaySuccess() ).error (data, status) -> $scope.displayFailure "Server returned with error status: " + status @@ -322,56 +319,14 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ if variant.hasOwnProperty("unit_value_with_description") match = variant.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/) if match - product = $scope.findProduct(product.id) + product = $scope.findProduct(product.id, $scope.products) variant.unit_value = parseFloat(match[1]) variant.unit_value = null if isNaN(variant.unit_value) variant.unit_value *= product.variant_unit_scale if variant.unit_value && product.variant_unit_scale variant.unit_description = match[3] - - $scope.productListsMatch = (clientProducts, serverProducts) -> - $scope.copyNewVariantIds clientProducts, serverProducts - angular.toJson($scope.productsWithoutDerivedAttributes(clientProducts)) == angular.toJson($scope.productsWithoutDerivedAttributes(serverProducts)) - - - # When variants are created clientside, they are given a negative id. The server - # responds with a real id, which would cause the productListsMatch() check to fail. - # To avoid that false negative, we copy the server variant id to the client for any - # negative ids. - $scope.copyNewVariantIds = (clientProducts, serverProducts) -> - if clientProducts? - for product, i in clientProducts - if product.variants? - for variant, j in product.variants - if variant.id < 0 - variant.id = serverProducts[i].variants[j].id - - - $scope.productsWithoutDerivedAttributes = (products) -> - products_filtered = [] - if products - products_filtered = $scope.deepCopyProducts products - for product in products_filtered - delete product.variant_unit_with_scale - if product.variants - for variant in product.variants - delete variant.unit_value_with_description - # If we end up live-updating this field, we might want to reinstate its verification here - delete variant.options_text - delete product.master - products_filtered - - - $scope.deepCopyProducts = (products) -> - copied_products = (angular.extend {}, product for product in products) - for product in copied_products - if product.variants - product.variants = (angular.extend {}, variant for variant in product.variants) - copied_products - - - $scope.findProduct = (id) -> - products = (product for product in $scope.products when product.id == id) + $scope.findProduct = (id, product_list) -> + products = (product for product in product_list when product.id == id) if products.length == 0 then null else products[0] diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 44466b5d10..88da46d0e2 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -397,8 +397,9 @@ describe "AdminProductEditCtrl", -> $scope.loadVariantUnit product expect(product.variant_unit_with_scale).toEqual "items" - it "loads data for variants (inc. master)", -> - spyOn $scope, "loadVariantVariantUnit" + it "loads data for variants (incl. master)", -> + spyOn $scope, "loadVariantUnitValues" + spyOn $scope, "loadVariantUnitValue" product = variant_unit_scale: 1.0 @@ -406,15 +407,27 @@ describe "AdminProductEditCtrl", -> variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] $scope.loadVariantUnit product - expect($scope.loadVariantVariantUnit).toHaveBeenCalledWith product, product.variants[0] - expect($scope.loadVariantVariantUnit).toHaveBeenCalledWith product, product.master + expect($scope.loadVariantUnitValues).toHaveBeenCalledWith product + expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.master + + it "loads data for variants (excl. master)", -> + spyOn $scope, "loadVariantUnitValue" + + product = + variant_unit_scale: 1.0 + master: {id: 1, unit_value: 1, unit_description: '(one)'} + variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] + $scope.loadVariantUnitValues product + + expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0] + expect($scope.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master describe "setting variant unit_value_with_description", -> it "sets by combining unit_value and unit_description", -> product = variant_unit_scale: 1.0 variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0]).toEqual id: 1 unit_value: 1 @@ -425,28 +438,28 @@ describe "AdminProductEditCtrl", -> product = variant_unit_scale: 1.0 variants: [{id: 1, unit_value: 1}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0].unit_value_with_description).toEqual '1' it "uses unit_description when value is missing", -> product = variant_unit_scale: 1.0 variants: [{id: 1, unit_description: 'Small'}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0].unit_value_with_description).toEqual 'Small' it "converts values from base value to chosen unit", -> product = variant_unit_scale: 1000.0 variants: [{id: 1, unit_value: 2500}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0].unit_value_with_description).toEqual '2.5' it "displays a unit_value of zero", -> product = variant_unit_scale: 1.0 variants: [{id: 1, unit_value: 0}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0].unit_value_with_description).toEqual '0' @@ -827,6 +840,8 @@ describe "AdminProductEditCtrl", -> it "runs displaySuccess() when post returns success", -> spyOn $scope, "displaySuccess" + spyOn $scope, "updateVariantLists" + spyOn DirtyProducts, "clear" $scope.products = [ { id: 1 @@ -851,14 +866,8 @@ describe "AdminProductEditCtrl", -> $httpBackend.flush() $timeout.flush() expect($scope.displaySuccess).toHaveBeenCalled() - - it "runs displayFailure() when post return data does not match $scope.products", -> - spyOn $scope, "displayFailure" - $scope.products = "current list of products" - $httpBackend.expectPOST("/admin/products/bulk_update").respond 200, "returned list of products" - $scope.updateProducts "updated list of products" - $httpBackend.flush() - expect($scope.displayFailure).toHaveBeenCalled() + expect(DirtyProducts.clear).toHaveBeenCalled() + expect($scope.updateVariantLists).toHaveBeenCalled() it "runs displayFailure() when post returns error", -> spyOn $scope, "displayFailure" @@ -868,87 +877,15 @@ describe "AdminProductEditCtrl", -> $httpBackend.flush() expect($scope.displayFailure).toHaveBeenCalled() - - describe "copying new variant ids from server to client", -> - it "copies server ids to the client where the client id is negative", -> - clientProducts = [ - { - id: 123 - variants: [{id: 1}, {id: -2}, {id: -3}] - } - ] - serverProducts = [ - { - id: 123 - variants: [{id: 1}, {id: 4534}, {id: 3453}] - } - ] - $scope.copyNewVariantIds(clientProducts, serverProducts) - expect(clientProducts).toEqual(serverProducts) - - - describe "fetching products without derived attributes", -> - it "returns products without the variant_unit_with_scale field", -> - $scope.products = [{id: 123, variant_unit_with_scale: 'weight_1000'}] - expect($scope.productsWithoutDerivedAttributes($scope.products)).toEqual([{id: 123}]) - - it "returns an empty array when products are undefined", -> - expect($scope.productsWithoutDerivedAttributes($scope.products)).toEqual([]) - - it "does not alter original products", -> - $scope.products = [{ - id: 123 - variant_unit_with_scale: 'weight_1000' - variants: [{options_text: 'foo'}] - }] - $scope.productsWithoutDerivedAttributes($scope.products) - expect($scope.products).toEqual [{ - id: 123 - variant_unit_with_scale: 'weight_1000' - variants: [{options_text: 'foo'}] - }] - - describe "updating variants", -> - it "returns variants without the unit_value_with_description field", -> - $scope.products = [{id: 123, variants: [{id: 234, unit_value_with_description: 'foo'}]}] - expect($scope.productsWithoutDerivedAttributes($scope.products)).toEqual [ - { - id: 123 - variants: [{id: 234}] - } - ] - - it "removes the master variant", -> - $scope.products = [{id: 123, master: {id: 234, unit_value_with_description: 'foo'}}] - expect($scope.productsWithoutDerivedAttributes($scope.products)).toEqual [ - { - id: 123 - } - ] - - - describe "deep copying products", -> - it "copies products", -> - product = {id: 123} - copiedProducts = $scope.deepCopyProducts [product] - expect(copiedProducts[0]).not.toBe(product) - - it "copies variants", -> - variant = {id: 1} - product = {id: 123, variants: [variant]} - copiedProducts = $scope.deepCopyProducts [product] - expect(copiedProducts[0].variants[0]).not.toBe(variant) - - describe "fetching a product by id", -> it "returns the product when it is present", -> product = {id: 123} $scope.products = [product] - expect($scope.findProduct(123)).toEqual product + expect($scope.findProduct(123, $scope.products)).toEqual product it "returns null when the product is not present", -> $scope.products = [] - expect($scope.findProduct(123)).toBeNull() + expect($scope.findProduct(123, $scope.products)).toBeNull() describe "adding variants", -> From 1abbc7fa860806a51c0e2f918b32a319a01fdf37 Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 31 Jul 2014 10:25:05 +1000 Subject: [PATCH 094/205] Switch data injection for BPE over to AMS --- .../admin/bulk_product_update.js.coffee | 29 +++++++++--------- .../api/products_controller_decorator.rb | 4 +++ .../spree/api/product_serializer.rb | 30 +++++++++++++++++++ .../spree/api/variant_serializer.rb | 13 ++++++++ .../spree/admin/products/bulk_edit.html.haml | 8 ++--- config/routes.rb | 1 + .../spree/product_serializer_spec.rb | 7 +++++ .../spree/variant_serializer_spec.rb | 7 +++++ 8 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 app/serializers/spree/api/product_serializer.rb create mode 100644 app/serializers/spree/api/variant_serializer.rb create mode 100644 spec/serializers/spree/product_serializer_spec.rb create mode 100644 spec/serializers/spree/variant_serializer_spec.rb diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index e0d67d1747..db1d078d73 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -6,7 +6,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ style: {} $scope.columns = - supplier: {name: "Supplier", visible: true} + producer: {name: "Producer", visible: true} name: {name: "Name", visible: true} unit: {name: "Unit", visible: true} price: {name: "Price", visible: true} @@ -17,7 +17,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.variant_unit_options = VariantUnitManager.variantUnitOptions() $scope.filterableColumns = [ - { name: "Supplier", db_column: "supplier_name" }, + { name: "Producer", db_column: "producer_name" }, { name: "Name", db_column: "name" } ] @@ -57,21 +57,20 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ if $scope.spree_api_key_ok $http.defaults.headers.common["X-Spree-Token"] = spree_api_key dataFetcher("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").then (data) -> - $scope.suppliers = data - # Need to have suppliers before we get products so we can match suppliers to product.supplier + $scope.producers = data + # Need to have producers before we get products so we can match producers to product.producer $scope.fetchProducts() else if authorise_api_reponse.hasOwnProperty("error") $scope.api_error_msg = authorise_api_reponse("error") else api_error_msg = "You don't have an API key yet. An attempt was made to generate one, but you are currently not authorised, please contact your site administrator for access." - $scope.fetchProducts = -> # WARNING: returns a promise $scope.loading = true queryString = $scope.currentFilters.reduce (qs,f) -> return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};" , "" - return dataFetcher("/api/products/managed?template=bulk_index;page=1;per_page=500;#{queryString}").then (data) -> + return dataFetcher("/api/products/bulk_products?page=1;per_page=500;#{queryString}").then (data) -> $scope.resetProducts data $scope.loading = false @@ -88,15 +87,15 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.unpackProduct = (product) -> $scope.displayProperties ||= {} $scope.displayProperties[product.id] ||= showVariants: false - $scope.matchSupplier product + $scope.matchProducer product $scope.loadVariantUnit product - $scope.matchSupplier = (product) -> - for i of $scope.suppliers - supplier = $scope.suppliers[i] - if angular.equals(supplier, product.supplier) - product.supplier = supplier + $scope.matchProducer = (product) -> + for i of $scope.producers + producer = $scope.producers[i] + if angular.equals(producer.id, product.producer) + product.producer = producer break @@ -252,7 +251,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.hasVariants = (product) -> - Object.keys(product.variants).length > 0 + product.variants.length > 0 $scope.hasUnit = (product) -> @@ -397,8 +396,8 @@ filterSubmitProducts = (productsToFilter) -> if product.hasOwnProperty("name") filteredProduct.name = product.name hasUpdatableProperty = true - if product.hasOwnProperty("supplier") - filteredProduct.supplier_id = product.supplier.id + if product.hasOwnProperty("producer") + filteredProduct.supplier_id = product.producer.id hasUpdatableProperty = true if product.hasOwnProperty("price") filteredProduct.price = product.price diff --git a/app/controllers/spree/api/products_controller_decorator.rb b/app/controllers/spree/api/products_controller_decorator.rb index 77c1aa6632..8e3c8f9c84 100644 --- a/app/controllers/spree/api/products_controller_decorator.rb +++ b/app/controllers/spree/api/products_controller_decorator.rb @@ -7,6 +7,10 @@ Spree::Api::ProductsController.class_eval do respond_with(@products, default_template: :index) end + def bulk_products + @products = product_scope.ransack(params[:q]).result.managed_by(current_api_user).page(params[:page]).per(params[:per_page]) + render text: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer).to_json + end def soft_delete authorize! :delete, Spree::Product diff --git a/app/serializers/spree/api/product_serializer.rb b/app/serializers/spree/api/product_serializer.rb new file mode 100644 index 0000000000..91afb85564 --- /dev/null +++ b/app/serializers/spree/api/product_serializer.rb @@ -0,0 +1,30 @@ +class Spree::Api::ProductSerializer < ActiveModel::Serializer + attributes :id, :name, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand + + attributes :taxon_ids, :on_hand, :price, :available_on, :permalink_live + + has_one :supplier, key: :producer, embed: :id + has_many :variants, key: :variants, embed: :ids#, serializer: Spree::Api::VariantSerializer + has_one :master, serializer: Spree::Api::VariantSerializer + + # Infinity is not a valid JSON object, but Rails encodes it anyway + def taxon_ids + object.taxons.map{ |t| t.id }.join(",") + end + + def on_hand + object.on_hand.nil? ? 0 : object.on_hand.to_f.finite? ? object.on_hand : "On demand" + end + + def price + object.price.nil? ? '0.0' : object.price + end + + def available_on + object.available_on.blank? ? "" : object.available_on.strftime("%F %T") + end + + def permalink_live + object.permalink + end +end \ No newline at end of file diff --git a/app/serializers/spree/api/variant_serializer.rb b/app/serializers/spree/api/variant_serializer.rb new file mode 100644 index 0000000000..67a696bdcc --- /dev/null +++ b/app/serializers/spree/api/variant_serializer.rb @@ -0,0 +1,13 @@ +class Spree::Api::VariantSerializer < ActiveModel::Serializer + attributes :id, :options_text, :unit_value, :unit_description, :on_demand, :display_as, :display_name + + attributes :on_hand, :price + + def on_hand + object.on_hand.nil? ? 0 : ( object.on_hand.to_f.finite? ? object.on_hand : "On demand" ) + end + + def price + object.price.nil? ? 0.to_f : object.price + end +end \ No newline at end of file diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index d1cdd6324f..39014dae64 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -104,7 +104,7 @@ %thead %tr %th.left-actions - %th{ 'ng-show' => 'columns.supplier.visible' } Supplier + %th{ 'ng-show' => 'columns.producer.visible' } Supplier %th{ 'ng-show' => 'columns.name.visible' } Name %th{ 'ng-show' => 'columns.unit.visible' } Unit / Value %th{ 'ng-show' => 'columns.unit.visible' } Display As @@ -120,8 +120,8 @@ %td.left-actions %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" } - %td.supplier{ 'ng-show' => 'columns.supplier.visible' } - %select.select2{ 'ng-model' => 'product.supplier', :name => 'supplier', 'ofn-track-product' => 'supplier', 'ng-options' => 's.name for s in suppliers' } + %td.producer{ 'ng-show' => 'columns.producer.visible' } + %select.select2{ 'ng-model' => 'product.producer', :name => 'producer', 'ofn-track-product' => 'producer', 'ng-options' => 'producer.name for producer in producers' } %td{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' } %td.unit{ 'ng-show' => 'columns.unit.visible' } @@ -150,7 +150,7 @@ %td.left-actions %a{ :class => "variant-item icon-caret-right", 'ng-hide' => "$last" } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last" } - %td{ 'ng-show' => 'columns.supplier.visible' } + %td{ 'ng-show' => 'columns.producer.visible' } %td{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => 'variant.display_name', :name => 'variant_display_name', 'ofn-track-variant' => 'display_name', :type => 'text', placeholder: "{{ product.name }}" } %td.unit_value{ 'ng-show' => 'columns.unit.visible' } diff --git a/config/routes.rb b/config/routes.rb index 75516552a4..af70141639 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -116,6 +116,7 @@ Spree::Core::Engine.routes.prepend do resources :products do get :managed, on: :collection + get :bulk_products, on: :collection delete :soft_delete resources :variants do diff --git a/spec/serializers/spree/product_serializer_spec.rb b/spec/serializers/spree/product_serializer_spec.rb new file mode 100644 index 0000000000..bbb65c1c7e --- /dev/null +++ b/spec/serializers/spree/product_serializer_spec.rb @@ -0,0 +1,7 @@ +describe Spree::Api::ProductSerializer do + let(:product) { create(:simple_product) } + it "serializes a product" do + serializer = Spree::Api::ProductSerializer.new product + serializer.to_json.should match product.name + end +end \ No newline at end of file diff --git a/spec/serializers/spree/variant_serializer_spec.rb b/spec/serializers/spree/variant_serializer_spec.rb new file mode 100644 index 0000000000..891298541f --- /dev/null +++ b/spec/serializers/spree/variant_serializer_spec.rb @@ -0,0 +1,7 @@ +describe Spree::Api::VariantSerializer do + let(:variant) { create(:variant) } + it "serializes a variant" do + serializer = Spree::Api::VariantSerializer.new variant + serializer.to_json.should match variant.name + end +end \ No newline at end of file From 9922dc6e797683f490e30b9f313f49c1160e86c3 Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 31 Jul 2014 10:42:34 +1000 Subject: [PATCH 095/205] Replace pagination with infinite scroll on BPE --- app/assets/javascripts/admin/admin.js.coffee | 2 +- app/assets/javascripts/admin/all.js | 1 + .../admin/bulk_product_update.js.coffee | 25 +++------------ .../spree/admin/products/bulk_edit.html.haml | 31 ++----------------- 4 files changed, 9 insertions(+), 50 deletions(-) diff --git a/app/assets/javascripts/admin/admin.js.coffee b/app/assets/javascripts/admin/admin.js.coffee index a0235a568f..c80e6252de 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"]).config ($httpProvider) -> +angular.module("ofn.admin", ["ngResource", "ngAnimate", "ofn.dropdown", "admin.products", "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, */*" \ No newline at end of file diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 4c763ec411..5ffb99ccb8 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -16,6 +16,7 @@ //= require admin/spree_auth //= require admin/spree_promo //= require admin/spree_paypal_express +//= require ../shared/ng-infinite-scroll.min.js //= require ./admin //= require ./enterprises/enterprises //= require ./payment_methods/payment_methods diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index db1d078d73..cbd6de7a30 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -30,24 +30,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ filters: { title: "Filter Products", visible: false } column_toggle: { title: "Toggle Columns", visible: false } - $scope.perPage = 25 - $scope.currentPage = 1 $scope.products = [] $scope.filteredProducts = [] $scope.currentFilters = [] - $scope.totalCount = -> $scope.filteredProducts.length - $scope.totalPages = -> Math.ceil($scope.totalCount()/$scope.perPage) - $scope.firstVisibleProduct = -> ($scope.currentPage-1)*$scope.perPage+1 - $scope.lastVisibleProduct = -> Math.min($scope.totalCount(),$scope.currentPage*$scope.perPage) - $scope.setPage = (page) -> $scope.currentPage = page - $scope.minPage = -> Math.max(1,Math.min($scope.totalPages()-4,$scope.currentPage-2)) - $scope.maxPage = -> Math.min($scope.totalPages(),Math.max(5,$scope.currentPage+2)) + $scope.limit = 15 $scope.productsWithUnsavedVariants = [] - $scope.$watch -> - $scope.totalPages() - , (newVal, oldVal) -> - $scope.currentPage = Math.max $scope.totalPages(), 1 if newVal != oldVal && $scope.totalPages() < $scope.currentPage $scope.initialise = (spree_api_key) -> authorise_api_reponse = "" @@ -328,6 +316,9 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ products = (product for product in product_list when product.id == id) if products.length == 0 then null else products[0] + $scope.incrementLimit = -> + if $scope.limit < $scope.products.length + $scope.limit = $scope.limit + 5 $scope.setMessage = (model, text, style, timeout) -> model.text = text @@ -464,11 +455,3 @@ toObjectWithIDKeys = (array) -> object[array[i].id].variants = toObjectWithIDKeys(array[i].variants) if array[i].hasOwnProperty("variants") and array[i].variants instanceof Array object - -subset = (bigArray,smallArray) -> - if smallArray instanceof Array && bigArray instanceof Array && smallArray.length > 0 - for item in smallArray - return false if angular.toJson(bigArray).indexOf(angular.toJson(item)) == -1 - return true - else - return false diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index 39014dae64..1d5835fa08 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -60,33 +60,8 @@ %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 500 || products.length == 0' } %div.quick_search{ :class => "five columns omega" } %input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } - %div.pagination{ :class => "seven columns omega" } - %div.pagenav{ :class => "two columns alpha" } - %span.first - %a{ :href => "#", 'ng-click' => "currentPage = 1", 'ng-show' => "currentPage > 1" } - « First - %span.prev - %a{ :href => "#", 'ng-click' => "currentPage = currentPage - 1", 'ng-show' => "currentPage > 1" } - ‹ Prev - %div.pagenav{ :class => "columns omega" } - %span.page{ 'ng-repeat' => "page in [] | rangeArray:minPage():maxPage()", 'ng-class' => "{current: currentPage==page}" } - %a{ :href => "#", 'ng-click' => "setPage(page)" } - {{page}} - %span{ 'ng-show' => "maxPage() < totalPages()" } ... - %div.pagenav{ :class => "two columns omega" } - %span.next - %a{ :href => "#", 'ng-click' => "currentPage = currentPage + 1", 'ng-show' => "currentPage < totalPages()" } - Next › - %span.last - %a{ :href => "#", 'ng-click' => "currentPage = totalPages()", 'ng-show' => "currentPage < totalPages()" } - Last » - %div.pagination_info{ :class => 'four columns alpha' } - Show  - %select{ 'ng-model' => 'perPage', :name => 'perPage', 'ng-options' => 'pp for pp in [25,50,100,200]'} -  per page - %br - %span Displaying {{firstVisibleProduct()}}-{{lastVisibleProduct()}} of {{totalCount()}} products - %table.index#listing_products.bulk + %div.spacer{ :class => "nine columns omega" }   + %table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" } %colgroup %col %col @@ -115,7 +90,7 @@ %th.actions %th.actions %th.actions - %tbody{ 'ng-repeat' => 'product in filteredProducts = (products | filter:query)', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", 'ng-show' => "$index >= perPage*(currentPage-1) && $index < perPage*currentPage" } + %tbody{ 'ng-repeat' => 'product in filteredProducts = ( products | filter:query | limitTo:limit )', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } %tr.product{ :id => "p_{{product.id}}" } %td.left-actions %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' } From b9f49344b444586839a6206bc67c49fc5bbe85f2 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 1 Aug 2014 11:47:11 +1000 Subject: [PATCH 096/205] BPE: Switch column toggle to dropdown, clean up specs --- .../admin/bulk_product_update.js.coffee | 3 +- .../admin/products_controller_decorator.rb | 2 +- .../spree/api/product_serializer.rb | 2 +- .../spree/admin/products/bulk_edit.html.haml | 64 +-- .../admin/bulk_product_update_spec.rb | 468 +++++++----------- 5 files changed, 205 insertions(+), 334 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index cbd6de7a30..6e708a705e 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -28,7 +28,6 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.optionTabs = filters: { title: "Filter Products", visible: false } - column_toggle: { title: "Toggle Columns", visible: false } $scope.products = [] $scope.filteredProducts = [] @@ -277,7 +276,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ filters: $scope.currentFilters ).success((data) -> DirtyProducts.clear() - #$scope.updateVariantLists(data) + $scope.updateVariantLists(data) $timeout -> $scope.displaySuccess() ).error (data, status) -> $scope.displayFailure "Server returned with error status: " + status diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 4f0c592284..1b01d88e4d 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -31,7 +31,7 @@ Spree::Admin::ProductsController.class_eval do end if product_set.save - redirect_to "/api/products/managed?template=bulk_index;page=1;per_page=500;#{bulk_index_query}" + redirect_to "/api/products/bulk_products?page=1;per_page=500;#{bulk_index_query}" else render :nothing => true, :status => 418 end diff --git a/app/serializers/spree/api/product_serializer.rb b/app/serializers/spree/api/product_serializer.rb index 91afb85564..8843cda3c5 100644 --- a/app/serializers/spree/api/product_serializer.rb +++ b/app/serializers/spree/api/product_serializer.rb @@ -4,7 +4,7 @@ class Spree::Api::ProductSerializer < ActiveModel::Serializer attributes :taxon_ids, :on_hand, :price, :available_on, :permalink_live has_one :supplier, key: :producer, embed: :id - has_many :variants, key: :variants, embed: :ids#, serializer: Spree::Api::VariantSerializer + has_many :variants, key: :variants, serializer: Spree::Api::VariantSerializer # embed: ids has_one :master, serializer: Spree::Api::VariantSerializer # Infinity is not a valid JSON object, but Rails encodes it anyway diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index 1d5835fa08..1e469792b2 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -46,10 +46,6 @@ {{ filter.value }} %div{ :class => "two columns omega" } %a{ :href => "#", 'ng-click' => "removeFilter(filter)" } Remove Filter - %div.column_toggle{ :class => "sixteen columns alpha", "ng-show" => 'optionTabs.column_toggle.visible' } - %ul.column-list{ :class => "sixteen columns alpha" } - %li.column-list-item{ :class => "three columns alpha", 'ofn-toggle-column' => 'column', 'ng-repeat' => 'column in columns' } - {{ column.name }} %hr %div.sixteen.columns.alpha.loading{ 'ng-show' => 'loading' } %h4 Loading Products... @@ -60,33 +56,41 @@ %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 500 || products.length == 0' } %div.quick_search{ :class => "five columns omega" } %input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } - %div.spacer{ :class => "nine columns omega" }   + %div{ :class => "eight columns" }   + %div{ :class => "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 }} %table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" } %colgroup - %col - %col - %col{'style' => 'width: 20%;'} - %col{'style' => 'width: 12%;'} - %col{'style' => 'width: 12%;'} - %col{'style' => 'width: 12%;'} - %col - %col - %col - %col - %col - %col + %col.actions + %col.producer{ 'style' => 'width: 14%;', 'ng-show' => 'columns.producer.visible' } + %col.name{ 'style' => 'width: 20%;', 'ng-show' => 'columns.name.visible' } + %col.unit{ 'style' => 'width: 14%;', 'ng-show' => 'columns.unit.visible' } + %col.display_as{ 'style' => 'width: 12%;', 'ng-show' => 'columns.unit.visible' } + %col.price{ 'style' => 'width: 10%;', 'ng-show' => 'columns.price.visible'} + %col.on_hand{ 'style' => 'width: 10%;', 'ng-show' => 'columns.on_hand.visible' } + %col.taxons{ 'ng-show' => 'columns.taxons.visible' } + %col.available_on{ 'ng-show' => 'columns.available_on.visible' } + %col.actions + %col.actions + %col.actions %thead %tr %th.left-actions - %th{ 'ng-show' => 'columns.producer.visible' } Supplier - %th{ 'ng-show' => 'columns.name.visible' } Name - %th{ 'ng-show' => 'columns.unit.visible' } Unit / Value - %th{ 'ng-show' => 'columns.unit.visible' } Display As - %th{ 'ng-show' => 'columns.price.visible' } Price - %th{ 'ng-show' => 'columns.on_hand.visible' } On Hand - %th{ 'ng-show' => 'columns.taxons.visible' } Taxons - %th{ 'ng-show' => 'columns.available_on.visible' } Av. On + %th.producer{ 'ng-show' => 'columns.producer.visible' } Producer + %th.name{ 'ng-show' => 'columns.name.visible' } Name + %th.unit{ 'ng-show' => 'columns.unit.visible' } Unit / Value + %th.display_as{ 'ng-show' => 'columns.unit.visible' } Display As + %th.price{ 'ng-show' => 'columns.price.visible' } Price + %th.on_hand{ 'ng-show' => 'columns.on_hand.visible' } On Hand + %th.taxons{ 'ng-show' => 'columns.taxons.visible' } Taxons + %th.available_on{ 'ng-show' => 'columns.available_on.visible' } Av. On %th.actions %th.actions %th.actions @@ -97,7 +101,7 @@ %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{ 'ng-model' => 'product.producer', :name => 'producer', 'ofn-track-product' => 'producer', 'ng-options' => 'producer.name for producer in producers' } - %td{ 'ng-show' => 'columns.name.visible' } + %td.name{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' } %td.unit{ 'ng-show' => 'columns.unit.visible' } %select.select2{ 'ng-model' => 'product.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-product' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' } @@ -106,14 +110,14 @@ %input{ 'ng-model' => 'product.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-product' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "product.variant_unit_with_scale == 'items'", :type => 'text' } %td.display_as{ 'ng-show' => 'columns.unit.visible' } %input{ 'ofn-display-as' => 'product.master', name: 'display_as', 'ofn-track-master' => 'display_as', type: 'text', placeholder: '{{ placeholder_text }}', ng: { hide: 'hasVariants(product)', model: 'product.master.display_as' } } - %td{ 'ng-show' => 'columns.price.visible' } + %td.price{ 'ng-show' => 'columns.price.visible' } %input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text', 'ng-hide' => 'hasVariants(product)' } - %td{ 'ng-show' => 'columns.on_hand.visible' } + %td.on_hand{ 'ng-show' => 'columns.on_hand.visible' } %span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' } %input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-hide' => 'hasVariants(product) || product.on_demand', :type => 'number' } %td{ 'ng-if' => 'columns.taxons.visible' } %input.fullwidth{ :type => 'text', 'ng-model' => 'product.taxon_ids', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'taxon_ids' } - %td{ 'ng-show' => 'columns.available_on.visible' } + %td.available_on{ 'ng-show' => 'columns.available_on.visible' } %input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ofn-track-product' => 'available_on', 'datetimepicker' => 'product.available_on', type: "text" } %td.actions %a{ 'ng-click' => 'editWarn(product)', :class => "edit-product icon-edit no-text" } @@ -121,7 +125,7 @@ %a{ 'ng-click' => 'cloneProduct(product)', :class => "clone-product icon-copy no-text" } %td.actions %a{ 'ng-click' => 'deleteProduct(product)', :class => "delete-product icon-trash no-text" } - %tr.variant{ 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'displayProperties[product.id].showVariants', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } + %tr.variant{ :id => "v_{{variant.id}}", 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'displayProperties[product.id].showVariants', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } %td.left-actions %a{ :class => "variant-item icon-caret-right", 'ng-hide' => "$last" } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last" } diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index ccd0ef479d..d69a8bc96a 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -18,14 +18,14 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_field "product_name", with: p1.name, :visible => true - page.should have_field "product_name", with: p2.name, :visible => true + expect(page).to have_field "product_name", with: p1.name, :visible => true + expect(page).to have_field "product_name", with: p2.name, :visible => true end it "displays a message when number of products is zero" do visit '/admin/products/bulk_edit' - page.should have_text "No matching products found." + expect(page).to have_text "No matching products found." end pending "displays a message when number of products is too great" do @@ -33,16 +33,7 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_text "Search returned too many products to display (500+), please apply more search filters to reduce the number of matching products" - end - - it "displays pagination information" do - p1 = FactoryGirl.create(:product) - p2 = FactoryGirl.create(:product) - - visit '/admin/products/bulk_edit' - - page.should have_text "Displaying 1-2 of 2 products" + expect(page).to have_text "Search returned too many products to display (500+), please apply more search filters to reduce the number of matching products" end it "displays a select box for suppliers, with the appropriate supplier selected" do @@ -54,8 +45,8 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "supplier", with_options: [s1.name,s2.name,s3.name], selected: s2.name - page.should have_select "supplier", with_options: [s1.name,s2.name,s3.name], selected: s3.name + expect(page).to have_select "producer", with_options: [s1.name,s2.name,s3.name], selected: s2.name + expect(page).to have_select "producer", with_options: [s1.name,s2.name,s3.name], selected: s3.name end it "displays a date input for available_on for each product, formatted to yyyy-mm-dd hh:mm:ss" do @@ -63,11 +54,11 @@ feature %q{ p2 = FactoryGirl.create(:product, available_on: Date.today-1) visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click - page.should have_field "available_on", with: p1.available_on.strftime("%F %T") - page.should have_field "available_on", with: p2.available_on.strftime("%F %T") + expect(page).to have_field "available_on", with: p1.available_on.strftime("%F %T") + expect(page).to have_field "available_on", with: p2.available_on.strftime("%F %T") end it "displays a price input for each product without variants (ie. for master variant)" do @@ -82,9 +73,9 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_field "price", with: "22.0" - page.should have_field "price", with: "44.0" - page.should_not have_field "price", with: "66.0", visible: true + expect(page).to have_field "price", with: "22.0" + expect(page).to have_field "price", with: "44.0" + expect(page).to have_no_field "price", with: "66.0", visible: true end it "displays an on hand count input for each product (ie. for master variant) if no regular variants exist" do @@ -97,9 +88,9 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should_not have_selector "span[name='on_hand']", text: "0" - page.should have_field "on_hand", with: "15" - page.should have_field "on_hand", with: "12" + expect(page).to have_no_selector "span[name='on_hand']", text: "0" + expect(page).to have_field "on_hand", with: "15" + expect(page).to have_field "on_hand", with: "12" end it "displays an on hand count in a span for each product (ie. for master variant) if other variants exist" do @@ -113,9 +104,9 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should_not have_field "on_hand", with: "15" - page.should have_selector "span[name='on_hand']", text: "4" - page.should have_field "on_hand", with: "12" + expect(page).to have_no_field "on_hand", with: "15" + expect(page).to have_selector "span[name='on_hand']", text: "4" + expect(page).to have_field "on_hand", with: "12" end it "displays 'on demand' for the on hand count when the product is available on demand" do @@ -124,8 +115,8 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_selector "span[name='on_hand']", text: "On demand" - page.should_not have_field "on_hand", visible: true + expect(page).to have_selector "span[name='on_hand']", text: "On demand" + expect(page).to have_no_field "on_hand", visible: true end it "displays 'on demand' for any variant that is available on demand" do @@ -136,10 +127,10 @@ feature %q{ visit '/admin/products/bulk_edit' first("a.view-variants").trigger('click') - page.should_not have_selector "span[name='on_hand']", text: "On demand", visible: true - page.should have_field "variant_on_hand", with: "4" - page.should_not have_field "variant_on_hand", with: "", visible: true - page.should have_selector "span[name='variant_on_hand']", text: "On demand" + expect(page).to have_no_selector "span[name='on_hand']", text: "On demand", visible: true + expect(page).to have_field "variant_on_hand", with: "4" + expect(page).to have_no_field "variant_on_hand", with: "", visible: true + expect(page).to have_selector "span[name='variant_on_hand']", text: "On demand" end it "displays a select box for the unit of measure for the product's variants" do @@ -147,7 +138,7 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: "Weight (g)" + expect(page).to have_select "variant_unit_with_scale", selected: "Weight (g)" end it "displays a text field for the item name when unit is set to 'Items'" do @@ -155,8 +146,8 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: "Items" - page.should have_field "variant_unit_name", with: "packet" + expect(page).to have_select "variant_unit_with_scale", selected: "Items" + expect(page).to have_field "variant_unit_name", with: "packet" end end @@ -170,13 +161,13 @@ feature %q{ v2 = FactoryGirl.create(:variant, display_name: "something2" ) visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" all("a.view-variants").each { |e| e.trigger('click') } - page.should have_field "product_name", with: v1.product.name - page.should have_field "product_name", with: v2.product.name - page.should have_field "variant_display_name", with: v1.display_name - page.should have_field "variant_display_name", with: v2.display_name + expect(page).to have_field "product_name", with: v1.product.name + expect(page).to have_field "product_name", with: v2.product.name + expect(page).to have_field "variant_display_name", with: v1.display_name + expect(page).to have_field "variant_display_name", with: v2.display_name end it "displays an on_hand input (for each variant) for each product" do @@ -187,9 +178,9 @@ feature %q{ visit '/admin/products/bulk_edit' all("a.view-variants").each { |e| e.trigger('click') } - page.should have_selector "span[name='on_hand']", text: "21" - page.should have_field "variant_on_hand", with: "15" - page.should have_field "variant_on_hand", with: "6" + expect(page).to have_selector "span[name='on_hand']", text: "21" + expect(page).to have_field "variant_on_hand", with: "15" + expect(page).to have_field "variant_on_hand", with: "6" end @@ -201,9 +192,9 @@ feature %q{ visit '/admin/products/bulk_edit' all("a.view-variants").each { |e| e.trigger('click') } - page.should have_field "price", with: "2.0", visible: false - page.should have_field "variant_price", with: "12.75" - page.should have_field "variant_price", with: "2.5" + expect(page).to have_field "price", with: "2.0", visible: false + expect(page).to have_field "variant_price", with: "12.75" + expect(page).to have_field "variant_price", with: "2.5" end it "displays a unit value field (for each variant) for each product" do @@ -214,10 +205,10 @@ feature %q{ visit '/admin/products/bulk_edit' all("a.view-variants").each { |e| e.trigger('click') } - page.should have_field "variant_unit_value_with_description", with: "1.2 (small bag)" - page.should have_field "variant_unit_value_with_description", with: "4.8 (large bag)" - page.should have_field "variant_display_as", with: "bag" - page.should have_field "variant_display_as", with: "bin" + expect(page).to have_field "variant_unit_value_with_description", with: "1.2 (small bag)" + expect(page).to have_field "variant_unit_value_with_description", with: "4.8 (large bag)" + expect(page).to have_field "variant_display_as", with: "bag" + expect(page).to have_field "variant_display_as", with: "bin" end end @@ -232,7 +223,7 @@ feature %q{ visit '/admin/products/bulk_edit' find("a", text: "NEW PRODUCT").click - page.should have_content 'NEW PRODUCT' + expect(page).to have_content 'NEW PRODUCT' fill_in 'product_name', :with => 'Big Bag Of Apples' select(s.name, :from => 'product_supplier_id') @@ -240,9 +231,9 @@ feature %q{ select taxon.name, from: 'product_primary_taxon_id' click_button 'Create' - URI.parse(current_url).path.should == '/admin/products/bulk_edit' - flash_message.should == 'Product "Big Bag Of Apples" has been successfully created!' - page.should have_field "product_name", with: 'Big Bag Of Apples' + expect(URI.parse(current_url).path).to eq '/admin/products/bulk_edit' + expect(flash_message).to eq 'Product "Big Bag Of Apples" has been successfully created!' + expect(page).to have_field "product_name", with: 'Big Bag Of Apples' end @@ -253,13 +244,13 @@ feature %q{ visit '/admin/products/bulk_edit' # I should not see an add variant button - page.should_not have_selector 'a.add-variant', visible: true + expect(page).to have_no_selector 'a.add-variant', visible: true # When I set the unit select "Weight (kg)", from: "variant_unit_with_scale" # I should see an add variant button - page.should have_selector 'a.add-variant', visible: true + expect(page).to have_selector 'a.add-variant', visible: true # When I add three variants page.find('a.add-variant', visible: true).trigger('click') @@ -267,13 +258,15 @@ feature %q{ page.find('a.add-variant', visible: true).trigger('click') # They should be added, and should see no edit buttons - page.all("tr.variant").count.should == 3 - page.should_not have_selector "a.edit-variant", visible: true + variant_count = page.all("tr.variant").count + expect(variant_count).to eq 3 + expect(page).to have_no_selector "a.edit-variant", visible: true # When I remove two, they should be removed page.all('a.delete-variant').first.click page.all('a.delete-variant').first.click - page.all("tr.variant").count.should == 1 + variant_count = page.all("tr.variant").count + expect(variant_count).to eq 1 # When I fill out variant details and hit update fill_in "variant_display_name", with: "Case of 12 Bottles" @@ -282,18 +275,18 @@ feature %q{ fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + expect(page.find("span#update-status-message")).to have_content "Update complete" updated_variant = Spree::Variant.where(deleted_at: nil).last - updated_variant.display_name.should == "Case of 12 Bottles" - updated_variant.unit_value.should == 3000 - updated_variant.unit_description.should == "(12x250 mL bottles)" - updated_variant.display_as.should == "Case" - updated_variant.price.should == 4.0 - updated_variant.on_hand.should == 10 + expect(updated_variant.display_name).to eq "Case of 12 Bottles" + expect(updated_variant.unit_value).to eq 3000 + expect(updated_variant.unit_description).to eq "(12x250 mL bottles)" + expect(updated_variant.display_as).to eq "Case" + expect(updated_variant.price).to eq 4.0 + expect(updated_variant.on_hand).to eq 10 # Then I should see edit buttons for the new variant - page.should have_selector "a.edit-variant", visible: true + expect(page).to have_selector "a.edit-variant", visible: true end @@ -309,18 +302,18 @@ feature %q{ visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click - page.should have_field "product_name", with: p.name - page.should have_select "supplier", selected: s1.name - page.should have_field "available_on", with: p.available_on.strftime("%F %T") - page.should have_field "price", with: "10.0" - page.should have_select "variant_unit_with_scale", selected: "Volume (L)" - page.should have_field "on_hand", with: "6" + expect(page).to have_field "product_name", with: p.name + expect(page).to have_select "producer", selected: s1.name + expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") + expect(page).to have_field "price", with: "10.0" + expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)" + expect(page).to have_field "on_hand", with: "6" fill_in "product_name", with: "Big Bag Of Potatoes" - select(s2.name, :from => 'supplier') + select(s2.name, :from => 'producer') fill_in "available_on", with: (Date.today-3).strftime("%F %T") fill_in "price", with: "20" select "Weight (kg)", from: "variant_unit_with_scale" @@ -328,17 +321,17 @@ feature %q{ fill_in "display_as", with: "Big Bag" click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + expect(page.find("span#update-status-message")).to have_content "Update complete" p.reload - p.name.should == "Big Bag Of Potatoes" - p.supplier.should == s2 - p.variant_unit.should == "weight" - p.variant_unit_scale.should == 1000 # Kg - p.available_on.should == 3.days.ago.beginning_of_day - p.master.display_as.should == "Big Bag" - p.price.should == 20.0 - p.on_hand.should == 18 + expect(p.name).to eq "Big Bag Of Potatoes" + expect(p.supplier).to eq s2 + expect(p.variant_unit).to eq "weight" + expect(p.variant_unit_scale).to eq 1000 # Kg + expect(p.available_on).to eq 3.days.ago.beginning_of_day + expect(p.master.display_as).to eq "Big Bag" + expect(p.price).to eq 20.0 + expect(p.on_hand).to eq 18 end scenario "updating a product with a variant unit of 'items'" do @@ -348,7 +341,7 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: "Weight (kg)" + expect(page).to have_select "variant_unit_with_scale", selected: "Weight (kg)" select "Items", from: "variant_unit_with_scale" fill_in "variant_unit_name", with: "loaf" @@ -370,7 +363,7 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: '' + expect(page).to have_select "variant_unit_with_scale", selected: '' select "Weight (kg)", from: "variant_unit_with_scale" first("a.view-variants").trigger('click') @@ -395,8 +388,8 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: '' - page.should_not have_field "master_unit_value_with_description", visible: true + expect(page).to have_select "variant_unit_with_scale", selected: '' + expect(page).to have_no_field "master_unit_value_with_description", visible: true select "Weight (kg)", from: "variant_unit_with_scale" fill_in "master_unit_value_with_description", with: '123 abc' @@ -420,7 +413,7 @@ feature %q{ visit '/admin/products/bulk_edit' select "Weight (kg)", from: "variant_unit_with_scale" - page.should_not have_field "master_unit_value_with_description", visible: true + expect(page).to have_no_field "master_unit_value_with_description", visible: true end end @@ -434,20 +427,20 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" first("a.view-variants").trigger('click') - page.should have_field "variant_price", with: "3.0" - page.should have_field "variant_unit_value_with_description", with: "250 (bottle)" - page.should have_field "variant_on_hand", with: "9" - page.should have_selector "span[name='on_hand']", text: "9" + expect(page).to have_field "variant_price", with: "3.0" + expect(page).to have_field "variant_unit_value_with_description", with: "250 (bottle)" + expect(page).to have_field "variant_on_hand", with: "9" + expect(page).to have_selector "span[name='on_hand']", text: "9" select "Volume (L)", from: "variant_unit_with_scale" fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" fill_in "variant_unit_value_with_description", with: "2 (8x250 mL bottles)" - page.should have_selector "span[name='on_hand']", text: "10" + expect(page).to have_selector "span[name='on_hand']", text: "10" click_button 'Update' page.find("span#update-status-message").should have_content "Update complete" @@ -466,10 +459,10 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" first("a.view-variants").trigger('click') - page.should have_field "variant_price", with: "3.0" + expect(page).to have_field "variant_price", with: "3.0" fill_in "variant_price", with: "10.0" @@ -486,7 +479,7 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_field "product_name", with: "original name" + expect(page).to have_field "product_name", with: "original name" fill_in "product_name", with: "new name 1" @@ -545,14 +538,14 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_selector "div.option_tab_titles h6", :text => "Filter Products" + expect(page).to have_selector "div.option_tab_titles h6", :text => "Filter Products" first("div.option_tab_titles h6", :text => "Filter Products").click select2_select "Name", from: "filter_property" select2_select "Contains", from: "filter_predicate" fill_in "filter_value", :with => "1" click_button "Apply Filter" - page.should_not have_field "product_name", with: p2.name + expect(page).to have_no_field "product_name", with: p2.name fill_in "product_name", :with => "new product1" click_on 'Update' @@ -561,22 +554,6 @@ feature %q{ p1.name.should == "new product1" end - scenario "updating a product when there are more products than the default API page size" do - p = FactoryGirl.create(:simple_product) - 25.times { FactoryGirl.create(:simple_product) } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - within "tr#p_#{p.id}" do - fill_in 'product_name', with: "new name" - end - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" - p.reload - p.name.should == "new name" - end - describe "using action buttons" do describe "using delete buttons" do it "shows a delete button for products, which deletes the appropriate product when clicked" do @@ -587,15 +564,15 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_selector "a.delete-product", :count => 3 + expect(page).to have_selector "a.delete-product", :count => 3 first("a.delete-product").click - page.should have_selector "a.delete-product", :count => 2 + expect(page).to have_selector "a.delete-product", :count => 2 visit '/admin/products/bulk_edit' - page.should have_selector "a.delete-product", :count => 2 + expect(page).to have_selector "a.delete-product", :count => 2 end it "shows a delete button for variants, which deletes the appropriate variant when clicked" do @@ -605,20 +582,20 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" all("a.view-variants").each { |e| e.trigger('click') } - page.should have_selector "a.delete-variant", :count => 3 + expect(page).to have_selector "a.delete-variant", :count => 3 first("a.delete-variant").click - page.should have_selector "a.delete-variant", :count => 2 + expect(page).to have_selector "a.delete-variant", :count => 2 visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" all("a.view-variants").select { |e| e.visible? }.each { |e| e.trigger('click') } - page.should have_selector "a.delete-variant", :count => 2 + expect(page).to have_selector "a.delete-variant", :count => 2 end end @@ -631,7 +608,7 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_selector "a.edit-product", :count => 3 + expect(page).to have_selector "a.edit-product", :count => 3 first("a.edit-product").click @@ -645,12 +622,14 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" all("a.view-variants").each { |e| e.trigger('click') } - page.should have_selector "a.edit-variant", :count => 3 + expect(page).to have_selector "a.edit-variant", :count => 3 - first("a.edit-variant").click + within "tr#v_#{v1.id}" do + first("a.edit-variant").click + end URI.parse(current_url).path.should == "/admin/products/#{v1.product.permalink}/variants/#{v1.id}/edit" end @@ -665,159 +644,48 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_selector "a.clone-product", :count => 3 + expect(page).to have_selector "a.clone-product", :count => 3 - first("a.clone-product").click - page.should have_selector "a.clone-product", :count => 4 - page.should have_field "product_name", with: "COPY OF #{p1.name}" - page.should have_select "supplier", selected: "#{p1.supplier.name}" + within "tr#p_#{p1.id}" do + first("a.clone-product").click + end + expect(page).to have_selector "a.clone-product", :count => 4 + expect(page).to have_field "product_name", with: "COPY OF #{p1.name}" + expect(page).to have_select "producer", selected: "#{p1.supplier.name}" visit '/admin/products/bulk_edit' - page.should have_selector "a.clone-product", :count => 4 - page.should have_field "product_name", with: "COPY OF #{p1.name}" - page.should have_select "supplier", selected: "#{p1.supplier.name}" + expect(page).to have_selector "a.clone-product", :count => 4 + expect(page).to have_field "product_name", with: "COPY OF #{p1.name}" + expect(page).to have_select "producer", selected: "#{p1.supplier.name}" end end end describe "using the page" do - describe "using tabs to hide and display page controls" do - it "shows a column display toggle button, which shows a list of columns when clicked" do - FactoryGirl.create(:simple_product) - login_to_admin_section - - visit '/admin/products/bulk_edit' - - page.should have_selector "div.column_toggle", :visible => false - - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Toggle Columns" - first("div.option_tab_titles h6", :text => "Toggle Columns").click - - page.should have_selector "div.option_tab_titles h6.selected", :text => "Toggle Columns" - page.should have_selector "div.column_toggle", :visible => true - page.should have_selector "li.column-list-item", text: "Available On" - - page.should have_selector "div.filters", :visible => false - - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Filter Products" - first("div.option_tab_titles h6", :text => "Filter Products").click - - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Toggle Columns" - page.should have_selector "div.option_tab_titles h6.selected", :text => "Filter Products" - page.should have_selector "div.filters", :visible => true - - first("div.option_tab_titles h6", :text => "Filter Products").click - - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Filter Products" - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Toggle Columns" - page.should have_selector "div.filters", :visible => false - page.should have_selector "div.column_toggle", :visible => false - end - end - - describe "using column display toggle" do - it "shows a column display toggle button, which shows a list of columns when clicked" do + describe "using column display dropdown" do + it "shows a column display dropdown, which shows a list of columns when clicked" do FactoryGirl.create(:simple_product) login_to_admin_section visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click - page.should have_selector "th", :text => "NAME" - page.should have_selector "th", :text => "SUPPLIER" - page.should have_selector "th", :text => "PRICE" - page.should have_selector "th", :text => "ON HAND" - page.should have_selector "th", :text => "AV. ON" + expect(page).to have_selector "th", :text => "NAME" + expect(page).to have_selector "th", :text => "PRODUCER" + expect(page).to have_selector "th", :text => "PRICE" + expect(page).to have_selector "th", :text => "ON HAND" + expect(page).to have_selector "th", :text => "AV. ON" - page.should have_selector "div.option_tab_titles h6", :text => "Toggle Columns" + first("div#columns_dropdown div.menu div.menu_item", text: /.{0,1}Producer/).click - page.should have_selector "div ul.column-list li.column-list-item", text: "Supplier" - first("li.column-list-item", text: "Supplier").click - - page.should_not have_selector "th", :text => "SUPPLIER" - page.should have_selector "th", :text => "NAME" - page.should have_selector "th", :text => "PRICE" - page.should have_selector "th", :text => "ON HAND" - page.should have_selector "th", :text => "AV. ON" - end - end - - describe "using pagination controls" do - it "shows pagination controls" do - 27.times { FactoryGirl.create(:product) } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - page.should have_select 'perPage', :selected => '25' - within '.pagination' do - page.should have_text "1 2" - page.should have_text "Next" - page.should have_text "Last" - end - end - - it "allows the number of visible products to be altered" do - 27.times { FactoryGirl.create(:product) } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - select '25', :from => 'perPage' - page.all("input[name='product_name']").select{ |e| e.visible? }.length.should == 25 - select '50', :from => 'perPage' - page.all("input[name='product_name']").select{ |e| e.visible? }.length.should == 27 - end - - it "displays the correct products when changing pages" do - 25.times { FactoryGirl.create(:product, :name => "page1product") } - 5.times { FactoryGirl.create(:product, :name => "page2product") } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - select '25', :from => 'perPage' - page.all("input[name='product_name']").select{ |e| e.visible? }.all?{ |e| e.value == "page1product" }.should == true - find("a", text: "2").click - page.all("input[name='product_name']").select{ |e| e.visible? }.all?{ |e| e.value == "page2product" }.should == true - end - - it "moves the user to the last available page when changing the number of pages in any way causes user to become orphaned" do - 50.times { FactoryGirl.create(:product) } - FactoryGirl.create(:product, :name => "fancy_product_name") - login_to_admin_section - - visit '/admin/products/bulk_edit' - - select '25', :from => 'perPage' - find("a", text: "3").click - select '50', :from => 'perPage' - page.first("div.pagenav span.page.current").should have_text "2" - page.all("input[name='product_name']", :visible => true).length.should == 1 - - select '25', :from => 'perPage' - fill_in "quick_filter", :with => "fancy_product_name" - page.first("div.pagenav span.page.current").should have_text "1" - page.all("input[name='product_name']", :visible => true).length.should == 1 - end - - it "paginates the filtered product list rather than all products" do - 25.times { FactoryGirl.create(:product, :name => "product_name") } - 3.times { FactoryGirl.create(:product, :name => "test_product_name") } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - select '25', :from => 'perPage' - page.should have_text "1 2" - fill_in "quick_filter", :with => "test_product_name" - page.all("input[name='product_name']", :visible => true).length.should == 3 - page.all("input[name='product_name']", :visible => true).all?{ |e| e.value == "test_product_name" }.should == true - page.should_not have_text "1 2" - page.should have_text "1" + expect(page).to have_no_selector "th", :text => "PRODUCER" + expect(page).to have_selector "th", :text => "NAME" + expect(page).to have_selector "th", :text => "PRICE" + expect(page).to have_selector "th", :text => "ON HAND" + expect(page).to have_selector "th", :text => "AV. ON" end end @@ -828,12 +696,12 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "div.option_tab_titles h6", :text => "Filter Products" + expect(page).to have_selector "div.option_tab_titles h6", :text => "Filter Products" first("div.option_tab_titles h6", :text => "Filter Products").click - page.should have_select "filter_property", visible: false - page.should have_select "filter_predicate", visible: false - page.should have_field "filter_value" + expect(page).to have_select "filter_property", visible: false + expect(page).to have_select "filter_predicate", visible: false + expect(page).to have_field "filter_value" end describe "clicking the 'Apply Filter' Button" do @@ -853,16 +721,16 @@ feature %q{ end it "adds a new filter to the list of applied filters" do - page.should have_text "Name Equals Product1" + expect(page).to have_text "Name Equals Product1" end it "displays the 'loading' splash" do - page.should have_selector "div.loading", :text => "Loading Products..." + expect(page).to have_selector "div.loading", :text => "Loading Products..." end it "loads appropriate products" do - page.should have_field "product_name", :with => "Product1" - page.should_not have_field "product_name", :with => "Product2" + expect(page).to have_field "product_name", :with => "Product1" + expect(page).to have_no_field "product_name", :with => "Product2" end describe "clicking the 'Remove Filter' link" do @@ -871,12 +739,12 @@ feature %q{ end it "removes the filter from the list of applied filters" do - page.should_not have_text "Name Equals Product1" + expect(page).to have_no_text "Name Equals Product1" end it "loads appropriate products" do - page.should have_field "product_name", :with => "Product1" - page.should have_field "product_name", :with => "Product2" + expect(page).to have_field "product_name", :with => "Product1" + expect(page).to have_field "product_name", :with => "Product2" end end end @@ -905,15 +773,15 @@ feature %q{ it "shows only products that I supply" do visit '/admin/products/bulk_edit' - page.should have_field 'product_name', with: product_supplied.name - page.should_not have_field 'product_name', with: product_not_supplied.name + expect(page).to have_field 'product_name', with: product_supplied.name + expect(page).to have_no_field 'product_name', with: product_not_supplied.name end it "shows only suppliers that I manage" do visit '/admin/products/bulk_edit' - page.should have_select 'supplier', with_options: [s1.name, s2.name], selected: s1.name - page.should_not have_select 'supplier', with_options: [s3.name] + expect(page).to have_select 'producer', with_options: [s1.name, s2.name], selected: s1.name + expect(page).to have_no_select 'producer', with_options: [s3.name] end it "shows inactive products that I supply" do @@ -921,24 +789,24 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_field 'product_name', with: product_supplied_inactive.name + expect(page).to have_field 'product_name', with: product_supplied_inactive.name end it "allows me to update a product" do p = product_supplied visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click - page.should have_field "product_name", with: p.name - page.should have_select "supplier", selected: s1.name - page.should have_field "available_on", with: p.available_on.strftime("%F %T") - page.should have_field "price", with: "10.0" - page.should have_field "on_hand", with: "6" + expect(page).to have_field "product_name", with: p.name + expect(page).to have_select "producer", selected: s1.name + expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") + expect(page).to have_field "price", with: "10.0" + expect(page).to have_field "on_hand", with: "6" fill_in "product_name", with: "Big Bag Of Potatoes" - select(s2.name, :from => 'supplier') + select(s2.name, :from => 'producer') fill_in "available_on", with: (Date.today-3).strftime("%F %T") fill_in "price", with: "20" select "Weight (kg)", from: "variant_unit_with_scale" @@ -946,17 +814,17 @@ feature %q{ fill_in "display_as", with: "Big Bag" click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + expect(page.find("span#update-status-message")).to have_content "Update complete" p.reload - p.name.should == "Big Bag Of Potatoes" - p.supplier.should == s2 - p.variant_unit.should == "weight" - p.variant_unit_scale.should == 1000 # Kg - p.available_on.should == 3.days.ago.beginning_of_day - p.master.display_as.should == "Big Bag" - p.price.should == 20.0 - p.on_hand.should == 18 + expect(p.name).to eq "Big Bag Of Potatoes" + expect(p.supplier).to eq s2 + expect(p.variant_unit).to eq "weight" + expect(p.variant_unit_scale).to eq 1000 # Kg + expect(p.available_on).to eq 3.days.ago.beginning_of_day + expect(p.master.display_as).to eq "Big Bag" + expect(p.price).to eq 20.0 + expect(p.on_hand).to eq 18 end end end From 27e992ebcbe3cbcf2dd4e4e10f4c991f0e2a14ca Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 1 Aug 2014 13:36:29 +1000 Subject: [PATCH 097/205] Staggered loading of products in BPE --- .../admin/bulk_product_update.js.coffee | 15 +++- .../api/products_controller_decorator.rb | 2 +- .../spree/admin/products/bulk_edit.html.haml | 6 +- .../unit/bulk_product_update_spec.js.coffee | 76 ++++++++----------- 4 files changed, 49 insertions(+), 50 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 6e708a705e..01556dec56 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -52,14 +52,23 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ else api_error_msg = "You don't have an API key yet. An attempt was made to generate one, but you are currently not authorised, please contact your site administrator for access." + $scope.$watch 'query', -> + $scope.limit = 15 # Reset limit whenever searching + $scope.fetchProducts = -> # WARNING: returns a promise $scope.loading = true queryString = $scope.currentFilters.reduce (qs,f) -> return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};" , "" - return dataFetcher("/api/products/bulk_products?page=1;per_page=500;#{queryString}").then (data) -> - $scope.resetProducts data + return dataFetcher("/api/products/bulk_products?page=1;per_page=20;#{queryString}").then (data) -> + $scope.resetProducts data.products $scope.loading = false + if data.pages > 1 + for page in [2..data.pages] + dataFetcher("/api/products/bulk_products?page=#{page};per_page=20;#{queryString}").then (data) -> + for product in data.products + $scope.unpackProduct product + $scope.products.push product $scope.resetProducts = (data) -> @@ -276,7 +285,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ filters: $scope.currentFilters ).success((data) -> DirtyProducts.clear() - $scope.updateVariantLists(data) + $scope.updateVariantLists(data.products) $timeout -> $scope.displaySuccess() ).error (data, status) -> $scope.displayFailure "Server returned with error status: " + status diff --git a/app/controllers/spree/api/products_controller_decorator.rb b/app/controllers/spree/api/products_controller_decorator.rb index 8e3c8f9c84..7e81e1f789 100644 --- a/app/controllers/spree/api/products_controller_decorator.rb +++ b/app/controllers/spree/api/products_controller_decorator.rb @@ -9,7 +9,7 @@ Spree::Api::ProductsController.class_eval do def bulk_products @products = product_scope.ransack(params[:q]).result.managed_by(current_api_user).page(params[:page]).per(params[:per_page]) - render text: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer).to_json + render text: { products: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer), pages: @products.num_pages }.to_json end def soft_delete diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index 1e469792b2..415ad7a81b 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -51,10 +51,10 @@ %h4 Loading Products... %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length == 0' } %h4{ :style => 'color:red;' } No matching products found. - %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length == 500' } + %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length >= 500' } %h6 Search returned too many products to display (500+), please apply more search filters to reduce the number of matching products - %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 500 || products.length == 0' } - %div.quick_search{ :class => "five columns omega" } + %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length >= 500 || products.length == 0' } + %div.quick_search{ :class => "five columns alpha" } %input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } %div{ :class => "eight columns" }   %div{ :class => "three columns omega" } diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 88da46d0e2..50370829d6 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -184,8 +184,7 @@ describe "filtering products for submission to database", -> created_at: null updated_at: null count_on_hand: 0 - supplier_id: 5 - supplier: + producer: id: 5 name: "Supplier 1" @@ -213,7 +212,7 @@ describe "filtering products for submission to database", -> expect(filterSubmitProducts([testProduct])).toEqual [ id: 1 name: "TestProduct" - supplier_id: 5 + producer: 5 variant_unit: 'volume' variant_unit_scale: 1 variant_unit_name: 'loaf' @@ -249,38 +248,45 @@ describe "AdminProductEditCtrl", -> ) describe "loading data upon initialisation", -> - it "gets a list of suppliers and then resets products with a list of data", -> + it "gets a list of producers and then resets products with a list of data", -> $httpBackend.expectGET("/api/users/authorise_api?token=api_key").respond success: "Use of API Authorised" - $httpBackend.expectGET("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").respond "list of suppliers" + $httpBackend.expectGET("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").respond "list of producers" spyOn($scope, "fetchProducts").andReturn "nothing" $scope.initialise "api_key" $httpBackend.flush() - expect($scope.suppliers).toEqual "list of suppliers" + expect($scope.producers).toEqual "list of producers" expect($scope.fetchProducts.calls.length).toEqual 1 expect($scope.spree_api_key_ok).toEqual true describe "fetching products", -> it "makes a standard call to dataFetcher when no filters exist", -> - $httpBackend.expectGET("/api/products/managed?template=bulk_index;page=1;per_page=500;").respond "list of products" + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products" $scope.fetchProducts() it "calls resetProducts after data has been received", -> spyOn $scope, "resetProducts" - $httpBackend.expectGET("/api/products/managed?template=bulk_index;page=1;per_page=500;").respond "list of products" + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: "list of products" } $scope.fetchProducts() $httpBackend.flush() expect($scope.resetProducts).toHaveBeenCalledWith "list of products" + it "calls makes more calls to dataFetcher if more pages exist", -> + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: [], pages: 2 } + $httpBackend.expectGET("/api/products/bulk_products?page=2;per_page=20;").respond { products: ["list of products"] } + $scope.fetchProducts() + $httpBackend.flush() + it "applies filters when they are present", -> filter = {property: $scope.filterableColumns[1], predicate:$scope.filterTypes[0], value:"Product1"} $scope.currentFilters.push filter # Don't use addFilter as that is not what we are testing expect($scope.currentFilters).toEqual [filter] - $httpBackend.expectGET("/api/products/managed?template=bulk_index;page=1;per_page=500;q[name_eq]=Product1;").respond "list of products" + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;q[name_eq]=Product1;").respond "list of products" $scope.fetchProducts() + $httpBackend.flush() it "sets the loading property to true before fetching products and unsets it when loading is complete", -> - $httpBackend.expectGET("/api/products/managed?template=bulk_index;page=1;per_page=500;").respond "list of products" + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products" $scope.fetchProducts() expect($scope.loading).toEqual true $httpBackend.flush() @@ -324,7 +330,7 @@ describe "AdminProductEditCtrl", -> describe "preparing products", -> beforeEach -> - spyOn $scope, "matchSupplier" + spyOn $scope, "matchProducer" spyOn $scope, "loadVariantUnit" it "initialises display properties for the product", -> @@ -333,11 +339,11 @@ describe "AdminProductEditCtrl", -> $scope.unpackProduct product expect($scope.displayProperties[123]).toEqual {showVariants: false} - it "calls matchSupplier for the product", -> + it "calls matchProducer for the product", -> product = {id: 123} $scope.displayProperties = {} $scope.unpackProduct product - expect($scope.matchSupplier.calls.length).toEqual 1 + expect($scope.matchProducer.calls.length).toEqual 1 it "calls loadVariantUnit for the product", -> product = {id: 123} @@ -346,31 +352,19 @@ describe "AdminProductEditCtrl", -> expect($scope.loadVariantUnit.calls.length).toEqual 1 - describe "matching supplier", -> - it "changes the supplier of the product to the one which matches it from the suppliers list", -> - s1_s = - id: 1 - name: "S1" - - s2_s = - id: 2 - name: "S2" - - s1_p = - id: 1 - name: "S1" - - expect(s1_s is s1_p).not.toEqual true - $scope.suppliers = [ - s1_s - s2_s + describe "matching producer", -> + it "changes the producer of the product to the one which matches it from the producers list", -> + $scope.producers = [ + { id: 1, name: "S1" } + { id: 2, name: "S2" } ] + product = id: 10 - supplier: s1_p + producer: 1 - $scope.matchSupplier product - expect(product.supplier is s1_s).toEqual true + $scope.matchProducer product + expect(product.producer).toBe $scope.producers[0] describe "loading variant unit", -> @@ -1059,7 +1053,7 @@ describe "AdminProductEditCtrl", -> $scope.cloneProduct $scope.products[0] $httpBackend.flush() - it "adds the newly created product to $scope.products and matches supplier", -> + it "adds the newly created product to $scope.products and matches producer", -> spyOn($scope, "unpackProduct").andCallThrough() $scope.products = [ id: 13 @@ -1069,8 +1063,7 @@ describe "AdminProductEditCtrl", -> product: id: 17 name: "new_product" - supplier: - id: 6 + producer: 6 variants: [ id: 3 @@ -1080,8 +1073,7 @@ describe "AdminProductEditCtrl", -> $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, id: 17 name: "new_product" - supplier: - id: 6 + producer: 6 variants: [ id: 3 @@ -1094,8 +1086,7 @@ describe "AdminProductEditCtrl", -> id: 17 name: "new_product" variant_unit_with_scale: null - supplier: - id: 6 + producer: 6 variants: [ id: 3 @@ -1112,8 +1103,7 @@ describe "AdminProductEditCtrl", -> id: 17 name: "new_product" variant_unit_with_scale: null - supplier: - id: 6 + producer: 6 variants: [ id: 3 From c139e2fab29703ba4b098bec0fb6661668d44bd0 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 1 Aug 2014 13:40:18 +1000 Subject: [PATCH 098/205] Remove 500 product warning --- app/views/spree/admin/products/bulk_edit.html.haml | 4 +--- spec/features/admin/bulk_product_update_spec.rb | 8 -------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index 415ad7a81b..99a5a9be02 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -51,9 +51,7 @@ %h4 Loading Products... %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length == 0' } %h4{ :style => 'color:red;' } No matching products found. - %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length >= 500' } - %h6 Search returned too many products to display (500+), please apply more search filters to reduce the number of matching products - %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length >= 500 || products.length == 0' } + %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0' } %div.quick_search{ :class => "five columns alpha" } %input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } %div{ :class => "eight columns" }   diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index d69a8bc96a..9acc1540eb 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -28,14 +28,6 @@ feature %q{ expect(page).to have_text "No matching products found." end - pending "displays a message when number of products is too great" do - 501.times { FactoryGirl.create(:simple_product) } - - visit '/admin/products/bulk_edit' - - expect(page).to have_text "Search returned too many products to display (500+), please apply more search filters to reduce the number of matching products" - end - it "displays a select box for suppliers, with the appropriate supplier selected" do s1 = FactoryGirl.create(:supplier_enterprise) s2 = FactoryGirl.create(:supplier_enterprise) From aaddccfae4764ef0bc5b9c5c8aa2f7a7b21f6bf9 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 1 Aug 2014 14:40:47 +1000 Subject: [PATCH 099/205] Injecting producer information into BPE --- .../javascripts/admin/bulk_product_update.js.coffee | 11 +++++------ .../spree/admin/products_controller_decorator.rb | 5 +++-- app/helpers/admin/injection_helper.rb | 4 ++++ app/views/spree/admin/products/bulk_edit.html.haml | 2 +- .../unit/bulk_product_update_spec.js.coffee | 6 ++++-- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 01556dec56..f76a1c37cf 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,10 +1,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ - "$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", - ($scope, $timeout, $http, dataFetcher, DirtyProducts, VariantUnitManager) -> + "$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", "producers", + ($scope, $timeout, $http, dataFetcher, DirtyProducts, VariantUnitManager, producers) -> $scope.updateStatusMessage = text: "" style: {} + $scope.producers = producers + $scope.columns = producer: {name: "Producer", visible: true} name: {name: "Name", visible: true} @@ -43,10 +45,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.spree_api_key_ok = data.hasOwnProperty("success") and data["success"] == "Use of API Authorised" if $scope.spree_api_key_ok $http.defaults.headers.common["X-Spree-Token"] = spree_api_key - dataFetcher("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").then (data) -> - $scope.producers = data - # Need to have producers before we get products so we can match producers to product.producer - $scope.fetchProducts() + $scope.fetchProducts() else if authorise_api_reponse.hasOwnProperty("error") $scope.api_error_msg = authorise_api_reponse("error") else diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 1b01d88e4d..c49b0aae97 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -1,5 +1,5 @@ Spree::Admin::ProductsController.class_eval do - before_filter :load_spree_api_key, :only => :bulk_edit + before_filter :load_bpe_data, :only => :bulk_edit alias_method :location_after_save_original, :location_after_save @@ -78,8 +78,9 @@ Spree::Admin::ProductsController.class_eval do private - def load_spree_api_key + def load_bpe_data current_user.generate_spree_api_key! unless spree_current_user.spree_api_key @spree_api_key = spree_current_user.spree_api_key + @producers = Enterprise.managed_by(spree_current_user).is_primary_producer end end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 4bbff9bf79..f6f8e05c6a 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -12,6 +12,10 @@ module Admin admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer end + def admin_inject_producers + admin_inject_json_ams_array "ofn.admin", "producers", @producers, Api::Admin::IdNameSerializer + end + def admin_inject_json_ams(ngModule, name, data, serializer, opts = {}) json = serializer.new(data).to_json render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index 99a5a9be02..d3ce04e12a 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -12,7 +12,7 @@ %div#new_product(data-hook) - +=admin_inject_producers %div{ 'ng-app' => 'ofn.admin', 'ng-controller' => 'AdminProductEditCtrl', 'ng-init' => "initialise('#{@spree_api_key}');loading=true;" } %div{ 'ng-show' => '!spree_api_key_ok' } {{ api_error_msg }} diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 50370829d6..ee852893d2 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -237,6 +237,10 @@ describe "AdminProductEditCtrl", -> beforeEach -> module "ofn.admin" + module ($provide)-> + $provide.value "producers", [] + null + beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _DirtyProducts_) -> $scope = $rootScope.$new() $ctrl = _$controller_ @@ -250,11 +254,9 @@ describe "AdminProductEditCtrl", -> describe "loading data upon initialisation", -> it "gets a list of producers and then resets products with a list of data", -> $httpBackend.expectGET("/api/users/authorise_api?token=api_key").respond success: "Use of API Authorised" - $httpBackend.expectGET("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").respond "list of producers" spyOn($scope, "fetchProducts").andReturn "nothing" $scope.initialise "api_key" $httpBackend.flush() - expect($scope.producers).toEqual "list of producers" expect($scope.fetchProducts.calls.length).toEqual 1 expect($scope.spree_api_key_ok).toEqual true From 2f5b5e7ee53b5b1d8b36ce0c6df9f008eca7149e Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 1 Aug 2014 16:19:20 +1000 Subject: [PATCH 100/205] Inject taxons into BPE --- .../admin/bulk_product_update.js.coffee | 13 ++++++------- .../directives/taxon_autocomplete.js.coffee | 10 ++++------ .../filters/taxons_term_filter.js.coffee | 7 +++++++ .../admin/services/taxons.js.coffee | 17 ++++++++--------- .../admin/products_controller_decorator.rb | 1 + app/helpers/admin/injection_helper.rb | 4 ++++ app/serializers/api/admin/taxon_serializer.rb | 3 +++ .../spree/api/product_serializer.rb | 1 - .../spree/api/variant_serializer.rb | 19 +++++++++---------- .../spree/admin/products/bulk_edit.html.haml | 1 + .../unit/bulk_product_update_spec.js.coffee | 1 + 11 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 app/assets/javascripts/admin/filters/taxons_term_filter.js.coffee create mode 100644 app/serializers/api/admin/taxon_serializer.rb diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index f76a1c37cf..5159234615 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,12 +1,10 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ - "$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", "producers", - ($scope, $timeout, $http, dataFetcher, DirtyProducts, VariantUnitManager, producers) -> + "$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", "producers", "taxons", + ($scope, $timeout, $http, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons) -> $scope.updateStatusMessage = text: "" style: {} - $scope.producers = producers - $scope.columns = producer: {name: "Producer", visible: true} name: {name: "Name", visible: true} @@ -31,6 +29,9 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.optionTabs = filters: { title: "Filter Products", visible: false } + + $scope.producers = producers + $scope.taxons = Taxons.taxons $scope.products = [] $scope.filteredProducts = [] $scope.currentFilters = [] @@ -87,13 +88,11 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.matchProducer = (product) -> - for i of $scope.producers - producer = $scope.producers[i] + for producer in $scope.producers if angular.equals(producer.id, product.producer) product.producer = producer break - $scope.loadVariantUnit = (product) -> product.variant_unit_with_scale = if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items' diff --git a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee index e5713274ef..0a1c01d38f 100644 --- a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee @@ -7,15 +7,13 @@ angular.module("ofn.admin").directive "ofnTaxonAutocomplete", (Taxons) -> placeholder: Spree.translations.taxon_placeholder multiple: true initSelection: (element, callback) -> - Taxons.findByIDs(element.val()).$promise.then (result) -> - callback Taxons.cleanTaxons(result) + callback Taxons.findByIDs(element.val()) query: (query) -> - Taxons.findByTerm(query.term).$promise.then (result) -> - query.callback { results: Taxons.cleanTaxons(result) } + query.callback { results: Taxons.findByTerm(query.term) } formatResult: (taxon) -> - taxon.pretty_name + taxon.name formatSelection: (taxon) -> - taxon.pretty_name + taxon.name element.on "change", -> scope.$apply -> ngModel.$setViewValue element.val() \ No newline at end of file diff --git a/app/assets/javascripts/admin/filters/taxons_term_filter.js.coffee b/app/assets/javascripts/admin/filters/taxons_term_filter.js.coffee new file mode 100644 index 0000000000..78a54175bf --- /dev/null +++ b/app/assets/javascripts/admin/filters/taxons_term_filter.js.coffee @@ -0,0 +1,7 @@ +angular.module("ofn.admin").filter "taxonsTermFilter", -> + return (lineItems,selectedSupplier,selectedDistributor,selectedOrderCycle) -> + filtered = [] + filtered.push lineItem for lineItem in lineItems when (angular.equals(selectedSupplier,"0") || lineItem.supplier.id == selectedSupplier) && + (angular.equals(selectedDistributor,"0") || lineItem.order.distributor.id == selectedDistributor) && + (angular.equals(selectedOrderCycle,"0") || lineItem.order.order_cycle.id == selectedOrderCycle) + filtered \ No newline at end of file diff --git a/app/assets/javascripts/admin/services/taxons.js.coffee b/app/assets/javascripts/admin/services/taxons.js.coffee index 1779ec3518..211ddb57a7 100644 --- a/app/assets/javascripts/admin/services/taxons.js.coffee +++ b/app/assets/javascripts/admin/services/taxons.js.coffee @@ -1,13 +1,12 @@ -angular.module("ofn.admin").factory "Taxons", ($resource) -> - resource = $resource "/admin/taxons/search" +angular.module("ofn.admin").factory "Taxons", (taxons, $filter) -> + new class Taxons + constructor: -> + @taxons = taxons - return { findByIDs: (ids) -> - resource.get { ids: ids } + taxons = [] + taxons.push taxon for taxon in @taxons when taxon.id.toString() in ids.split(",") + taxons findByTerm: (term) -> - resource.get { q: term } - - cleanTaxons: (data) -> - data['taxons'].map (result) -> result - } \ No newline at end of file + $filter('filter')(@taxons, term) \ No newline at end of file diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index c49b0aae97..289c85d443 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -82,5 +82,6 @@ Spree::Admin::ProductsController.class_eval do current_user.generate_spree_api_key! unless spree_current_user.spree_api_key @spree_api_key = spree_current_user.spree_api_key @producers = Enterprise.managed_by(spree_current_user).is_primary_producer + @taxons = Spree::Taxon.order(:name) end end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index f6f8e05c6a..481e7912c6 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -16,6 +16,10 @@ module Admin admin_inject_json_ams_array "ofn.admin", "producers", @producers, Api::Admin::IdNameSerializer end + def admin_inject_taxons + admin_inject_json_ams_array "ofn.admin", "taxons", @taxons, Api::Admin::TaxonSerializer + end + def admin_inject_json_ams(ngModule, name, data, serializer, opts = {}) json = serializer.new(data).to_json render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} diff --git a/app/serializers/api/admin/taxon_serializer.rb b/app/serializers/api/admin/taxon_serializer.rb new file mode 100644 index 0000000000..61766c87cf --- /dev/null +++ b/app/serializers/api/admin/taxon_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::TaxonSerializer < ActiveModel::Serializer + attributes :id, :name, :pretty_name +end \ No newline at end of file diff --git a/app/serializers/spree/api/product_serializer.rb b/app/serializers/spree/api/product_serializer.rb index 8843cda3c5..eb72002dcc 100644 --- a/app/serializers/spree/api/product_serializer.rb +++ b/app/serializers/spree/api/product_serializer.rb @@ -7,7 +7,6 @@ class Spree::Api::ProductSerializer < ActiveModel::Serializer has_many :variants, key: :variants, serializer: Spree::Api::VariantSerializer # embed: ids has_one :master, serializer: Spree::Api::VariantSerializer - # Infinity is not a valid JSON object, but Rails encodes it anyway def taxon_ids object.taxons.map{ |t| t.id }.join(",") end diff --git a/app/serializers/spree/api/variant_serializer.rb b/app/serializers/spree/api/variant_serializer.rb index 67a696bdcc..f594a540ef 100644 --- a/app/serializers/spree/api/variant_serializer.rb +++ b/app/serializers/spree/api/variant_serializer.rb @@ -1,13 +1,12 @@ class Spree::Api::VariantSerializer < ActiveModel::Serializer attributes :id, :options_text, :unit_value, :unit_description, :on_demand, :display_as, :display_name - - attributes :on_hand, :price - - def on_hand - object.on_hand.nil? ? 0 : ( object.on_hand.to_f.finite? ? object.on_hand : "On demand" ) - end - - def price - object.price.nil? ? 0.to_f : object.price - end + attributes :on_hand, :price + + def on_hand + object.on_hand.nil? ? 0 : ( object.on_hand.to_f.finite? ? object.on_hand : "On demand" ) + end + + def price + object.price.nil? ? 0.to_f : object.price + end end \ No newline at end of file diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index d3ce04e12a..f78a53ba19 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -13,6 +13,7 @@ =admin_inject_producers +=admin_inject_taxons %div{ 'ng-app' => 'ofn.admin', 'ng-controller' => 'AdminProductEditCtrl', 'ng-init' => "initialise('#{@spree_api_key}');loading=true;" } %div{ 'ng-show' => '!spree_api_key_ok' } {{ api_error_msg }} diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index ee852893d2..99f53c5079 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -239,6 +239,7 @@ describe "AdminProductEditCtrl", -> module "ofn.admin" module ($provide)-> $provide.value "producers", [] + $provide.value "taxons", [] null beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _DirtyProducts_) -> From d9692312acce1ecc85e2e237f0b6e43077fcc2e1 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 1 Aug 2014 17:20:48 +1000 Subject: [PATCH 101/205] WIP: swapping filters over to drop downs --- .../admin/bulk_product_update.js.coffee | 16 +++--- .../admin/products_controller_decorator.rb | 2 +- .../spree/api/product_serializer.rb | 1 + .../spree/admin/products/bulk_edit.html.haml | 55 +++++++------------ 4 files changed, 30 insertions(+), 44 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 5159234615..81704b17f0 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,5 +1,5 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ - "$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", "producers", "taxons", + "$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", "producers", "Taxons", ($scope, $timeout, $http, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons) -> $scope.updateStatusMessage = text: "" @@ -52,7 +52,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ else api_error_msg = "You don't have an API key yet. An attempt was made to generate one, but you are currently not authorised, please contact your site administrator for access." - $scope.$watch 'query', -> + $scope.$watchCollection '[query, producerFilter, categoryFilter]', -> $scope.limit = 15 # Reset limit whenever searching $scope.fetchProducts = -> # WARNING: returns a promise @@ -83,15 +83,15 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.unpackProduct = (product) -> $scope.displayProperties ||= {} $scope.displayProperties[product.id] ||= showVariants: false - $scope.matchProducer product + #$scope.matchProducer product $scope.loadVariantUnit product - $scope.matchProducer = (product) -> - for producer in $scope.producers - if angular.equals(producer.id, product.producer) - product.producer = producer - break + # $scope.matchProducer = (product) -> + # for producer in $scope.producers + # if angular.equals(producer.id, product.producer) + # product.producer = producer + # break $scope.loadVariantUnit = (product) -> product.variant_unit_with_scale = diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 289c85d443..8b3b42feea 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -81,7 +81,7 @@ Spree::Admin::ProductsController.class_eval do def load_bpe_data current_user.generate_spree_api_key! unless spree_current_user.spree_api_key @spree_api_key = spree_current_user.spree_api_key - @producers = Enterprise.managed_by(spree_current_user).is_primary_producer + @producers = Enterprise.managed_by(spree_current_user).is_primary_producer.order(:name) @taxons = Spree::Taxon.order(:name) end end diff --git a/app/serializers/spree/api/product_serializer.rb b/app/serializers/spree/api/product_serializer.rb index eb72002dcc..cc48b30929 100644 --- a/app/serializers/spree/api/product_serializer.rb +++ b/app/serializers/spree/api/product_serializer.rb @@ -4,6 +4,7 @@ class Spree::Api::ProductSerializer < ActiveModel::Serializer attributes :taxon_ids, :on_hand, :price, :available_on, :permalink_live has_one :supplier, key: :producer, embed: :id + has_one :primary_taxon, key: :category, embed: :id has_many :variants, key: :variants, serializer: Spree::Api::VariantSerializer # embed: ids has_one :master, serializer: Spree::Api::VariantSerializer diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index f78a53ba19..f00ed1c7c1 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -17,46 +17,31 @@ %div{ 'ng-app' => 'ofn.admin', 'ng-controller' => 'AdminProductEditCtrl', 'ng-init' => "initialise('#{@spree_api_key}');loading=true;" } %div{ 'ng-show' => '!spree_api_key_ok' } {{ api_error_msg }} - %div.option_tab_titles{ :class => "sixteen columns alpha" } - %h6{ :class => "three columns alpha", 'ng-repeat' => "tab in optionTabs", "ng-click" => "shiftTab(tab)", "ng-class" => "tab.visible && 'selected' || !tab.visible && 'unselected'"} - {{ tab.title }} - %div.option_tabs{ :class => "sixteen columns alpha" } - %div.filters{ :class => "sixteen columns alpha", "ng-show" => 'optionTabs.filters.visible' } - %div{ :class => "four columns alpha" } - Column: - %br.clear - %select.fullwidth{ 'ng-model' => 'filterProperty', :id => "filter_property", 'ng-options' => 'fc.name for fc in filterableColumns', 'ofn-select2-min-search' => 10 } - %div{ :class => "four columns omega" } - Filter Type: - %br.clear - %select.fullwidth{ 'ng-model' => 'filterPredicate', :id => "filter_predicate", 'ng-options' => 'ft.name for ft in filterTypes', 'ofn-select2-min-search' => 10 } - %div{ :class => "six columns omega" } - Value: - %br.clear - %input{ :class => "four columns alpha", 'ng-model' => 'filterValue', :id => "filter_value", :type => "text", 'placeholder' => 'Filter Value' } - %div{ :class => "two columns omega" } -   - %input.fullwidth{ :name => "add_filter", :value => "Apply Filter", :type => "button", "ng-click" => "addFilter({property:filterProperty,predicate:filterPredicate,value:filterValue})" } - %div.applied_filters{ :class => "sixteen columns alpha", "ng-show" => 'optionTabs.filters.visible && currentFilters.length > 0' } - %div.applied_filter{ :class => "sixteen columns alpha", 'ng-repeat' => 'filter in currentFilters' } - %div{ :class => "four columns alpha" } - {{ filter.property.name }} - %div{ :class => "four columns omega" } - {{ filter.predicate.name }} - %div{ :class => "six columns omega" } - {{ filter.value }} - %div{ :class => "two columns omega" } - %a{ :href => "#", 'ng-click' => "removeFilter(filter)" } Remove Filter - %hr %div.sixteen.columns.alpha.loading{ 'ng-show' => 'loading' } %h4 Loading Products... %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length == 0' } %h4{ :style => 'color:red;' } No matching products found. %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0' } - %div.quick_search{ :class => "five columns alpha" } + %div.quick_search{ :class => "four columns alpha" } + %label{ :for => 'quick_filter' } + %br %input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } - %div{ :class => "eight columns" }   + .filter_select{ :class => "three columns" } + %label{ :for => 'producer_filter' }Producer + %br + %select{ :class => "three columns alpha", :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in producers' } + .filter_select{ :class => "three columns" } + %label{ :for => 'taxon_filter' }Category + %br + %select{ :class => "three columns alpha", :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in taxons'} + .filter_clear{ :class => "two columns omega" } + %label{ :for => 'clear_all_filters' } + %br + %input.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "Clear All", 'ng-click' => "resetSelectFilters()" } + %div{ :class => "one column" }   %div{ :class => "three columns omega" } + %label{ } + %br %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'" } @@ -93,13 +78,13 @@ %th.actions %th.actions %th.actions - %tbody{ 'ng-repeat' => 'product in filteredProducts = ( products | filter:query | limitTo:limit )', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } + %tbody{ 'ng-repeat' => 'product in filteredProducts = ( products | filter:query | filter:{producer: producerFilter}:true | filter:{category: categoryFilter}:true | limitTo:limit )', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } %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{ :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{ 'ng-model' => 'product.producer', :name => 'producer', 'ofn-track-product' => 'producer', 'ng-options' => 'producer.name for producer in producers' } + %select.select2{ 'ng-model' => 'product.producer', :name => 'producer', 'ofn-track-product' => 'producer', 'ng-options' => 'producer.id as producer.name for producer in producers' } %td.name{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' } %td.unit{ 'ng-show' => 'columns.unit.visible' } From 59a3fe11add536c11c1792577997bfc2a47de2de Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 6 Aug 2014 10:41:07 +1000 Subject: [PATCH 102/205] Fix taxon service specs --- .../admin/services/taxons.js.coffee | 4 +- .../unit/admin/services/taxons_spec.js.coffee | 27 +++++++++++ .../unit/bulk_product_update_spec.js.coffee | 47 ------------------- 3 files changed, 28 insertions(+), 50 deletions(-) create mode 100644 spec/javascripts/unit/admin/services/taxons_spec.js.coffee diff --git a/app/assets/javascripts/admin/services/taxons.js.coffee b/app/assets/javascripts/admin/services/taxons.js.coffee index 211ddb57a7..a45770c787 100644 --- a/app/assets/javascripts/admin/services/taxons.js.coffee +++ b/app/assets/javascripts/admin/services/taxons.js.coffee @@ -4,9 +4,7 @@ angular.module("ofn.admin").factory "Taxons", (taxons, $filter) -> @taxons = taxons findByIDs: (ids) -> - taxons = [] - taxons.push taxon for taxon in @taxons when taxon.id.toString() in ids.split(",") - taxons + taxon for taxon in @taxons when taxon.id in ids.split(",") findByTerm: (term) -> $filter('filter')(@taxons, term) \ No newline at end of file diff --git a/spec/javascripts/unit/admin/services/taxons_spec.js.coffee b/spec/javascripts/unit/admin/services/taxons_spec.js.coffee new file mode 100644 index 0000000000..4c69052b38 --- /dev/null +++ b/spec/javascripts/unit/admin/services/taxons_spec.js.coffee @@ -0,0 +1,27 @@ +describe "Taxons service", -> + Taxons = taxons = $httpBackend = $resource = null + + beforeEach -> + module "ofn.admin" + module ($provide)-> + $provide.value "taxons", [{id: "1", name: "t1"}, {id: "2", name: "t2"}, {id: "12", name: "t12"}, {id: "31", name: "t31"}] + null + + beforeEach inject (_Taxons_, _$resource_, _$httpBackend_) -> + Taxons = _Taxons_ + $resource = _$resource_ + $httpBackend = _$httpBackend_ + + describe "calling findByIDs", -> + it "returns taxons with exactly matching ids", -> + result = Taxons.findByIDs("1,2") + expect(result).toEqual [{id: "1", name: "t1"}, {id: "2", name: "t2"}] + + it "ignores ids which do not exactly match", -> + result = Taxons.findByIDs("1,3") + expect(result).toEqual [{id: "1", name: "t1"}] + + describe "calling findByTerm", -> + it "returns taxons which match partially", -> + result = Taxons.findByTerm("t1") + expect(result).toEqual [{id: "1", name: "t1"}, {id: "12", name: "t12"}] \ No newline at end of file diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 99f53c5079..0a61ac2154 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -333,7 +333,6 @@ describe "AdminProductEditCtrl", -> describe "preparing products", -> beforeEach -> - spyOn $scope, "matchProducer" spyOn $scope, "loadVariantUnit" it "initialises display properties for the product", -> @@ -342,12 +341,6 @@ describe "AdminProductEditCtrl", -> $scope.unpackProduct product expect($scope.displayProperties[123]).toEqual {showVariants: false} - it "calls matchProducer for the product", -> - product = {id: 123} - $scope.displayProperties = {} - $scope.unpackProduct product - expect($scope.matchProducer.calls.length).toEqual 1 - it "calls loadVariantUnit for the product", -> product = {id: 123} $scope.displayProperties = {} @@ -355,21 +348,6 @@ describe "AdminProductEditCtrl", -> expect($scope.loadVariantUnit.calls.length).toEqual 1 - describe "matching producer", -> - it "changes the producer of the product to the one which matches it from the producers list", -> - $scope.producers = [ - { id: 1, name: "S1" } - { id: 2, name: "S2" } - ] - - product = - id: 10 - producer: 1 - - $scope.matchProducer product - expect(product.producer).toBe $scope.producers[0] - - describe "loading variant unit", -> describe "setting product variant_unit_with_scale field", -> it "sets by combining variant_unit and variant_unit_scale", -> @@ -1264,28 +1242,3 @@ describe "converting arrays of objects with ids to an object with ids as keys", expect(toObjectWithIDKeys).toHaveBeenCalledWith [id: 17] expect(toObjectWithIDKeys).not.toHaveBeenCalledWith {12: {id: 12}} - -describe "Taxons service", -> - Taxons = $httpBackend = $resource = null - - beforeEach -> - module "ofn.admin" - - beforeEach inject (_Taxons_, _$resource_, _$httpBackend_) -> - Taxons = _Taxons_ - $resource = _$resource_ - $httpBackend = _$httpBackend_ - - it "calling findByIDs makes a http request", -> - response = { taxons: "list of taxons by id" } - $httpBackend.expectGET("/admin/taxons/search?ids=1,2").respond 200, response - taxons = Taxons.findByIDs("1,2") - $httpBackend.flush() - expect(angular.equals(taxons,response)).toBe true - - it "calling findByTerm makes a http request", -> - response = { taxons: "list of taxons by term" } - $httpBackend.expectGET("/admin/taxons/search?q=lala").respond 200, response - taxons = Taxons.findByTerm("lala") - $httpBackend.flush() - expect(angular.equals(taxons,response)).toBe true From e76c2a215af6ae3e41587851ba57a5139005f003 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 6 Aug 2014 10:48:05 +1000 Subject: [PATCH 103/205] Fix BPE spec around supplier conversion in filtering --- app/assets/javascripts/admin/bulk_product_update.js.coffee | 2 +- spec/javascripts/unit/bulk_product_update_spec.js.coffee | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 81704b17f0..7206492c38 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -394,7 +394,7 @@ filterSubmitProducts = (productsToFilter) -> filteredProduct.name = product.name hasUpdatableProperty = true if product.hasOwnProperty("producer") - filteredProduct.supplier_id = product.producer.id + filteredProduct.supplier_id = product.producer hasUpdatableProperty = true if product.hasOwnProperty("price") filteredProduct.price = product.price diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 0a61ac2154..2543d5c0e8 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -184,9 +184,7 @@ describe "filtering products for submission to database", -> created_at: null updated_at: null count_on_hand: 0 - producer: - id: 5 - name: "Supplier 1" + producer: 5 group_buy: null group_buy_unit_size: null @@ -212,7 +210,7 @@ describe "filtering products for submission to database", -> expect(filterSubmitProducts([testProduct])).toEqual [ id: 1 name: "TestProduct" - producer: 5 + supplier_id: 5 variant_unit: 'volume' variant_unit_scale: 1 variant_unit_name: 'loaf' From 5f277dae61cb010d90070604b9f67d38f834d1e5 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 6 Aug 2014 11:33:43 +1000 Subject: [PATCH 104/205] Fixing BPE specs related to filtering --- .../admin/bulk_product_update_spec.rb | 80 +++++-------------- 1 file changed, 21 insertions(+), 59 deletions(-) diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 9acc1540eb..bc2e0d7b49 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -524,19 +524,16 @@ feature %q{ end scenario "updating when a filter has been applied" do - p1 = FactoryGirl.create(:simple_product, :name => "product1") - p2 = FactoryGirl.create(:simple_product, :name => "product2") + s1 = create(:supplier_enterprise) + s2 = create(:supplier_enterprise) + p1 = FactoryGirl.create(:simple_product, :name => "product1", supplier: s1) + p2 = FactoryGirl.create(:simple_product, :name => "product2", supplier: s2) login_to_admin_section visit '/admin/products/bulk_edit' - expect(page).to have_selector "div.option_tab_titles h6", :text => "Filter Products" - first("div.option_tab_titles h6", :text => "Filter Products").click + select2_select s1.name, from: "producer_filter" - select2_select "Name", from: "filter_property" - select2_select "Contains", from: "filter_predicate" - fill_in "filter_value", :with => "1" - click_button "Apply Filter" expect(page).to have_no_field "product_name", with: p2.name fill_in "product_name", :with => "new product1" @@ -682,63 +679,28 @@ feature %q{ end describe "using filtering controls" do - it "displays basic filtering controls" do - FactoryGirl.create(:simple_product) - + it "displays basic filtering controls which filter the product list" do + s1 = create(:supplier_enterprise) + s2 = create(:supplier_enterprise) + p1 = FactoryGirl.create(:simple_product, :name => "product1", supplier: s1) + p2 = FactoryGirl.create(:simple_product, :name => "product2", supplier: s2) login_to_admin_section + visit '/admin/products/bulk_edit' - expect(page).to have_selector "div.option_tab_titles h6", :text => "Filter Products" - first("div.option_tab_titles h6", :text => "Filter Products").click + # Page shows the filter controls + expect(page).to have_select "producer_filter", visible: false + expect(page).to have_select "category_filter", visible: false - expect(page).to have_select "filter_property", visible: false - expect(page).to have_select "filter_predicate", visible: false - expect(page).to have_field "filter_value" - end + # All products are shown when no filter is selected + expect(page).to have_field "product_name", with: p1.name + expect(page).to have_field "product_name", with: p2.name - describe "clicking the 'Apply Filter' Button" do - before(:each) do - FactoryGirl.create(:simple_product, :name => "Product1") - FactoryGirl.create(:simple_product, :name => "Product2") + select2_select s1.name, from: "producer_filter" - login_to_admin_section - visit '/admin/products/bulk_edit' - - first("div.option_tab_titles h6", :text => "Filter Products").click - - select2_select "Name", :from => "filter_property" - select2_select "Equals", :from => "filter_predicate" - fill_in "filter_value", :with => "Product1" - click_button "Apply Filter" - end - - it "adds a new filter to the list of applied filters" do - expect(page).to have_text "Name Equals Product1" - end - - it "displays the 'loading' splash" do - expect(page).to have_selector "div.loading", :text => "Loading Products..." - end - - it "loads appropriate products" do - expect(page).to have_field "product_name", :with => "Product1" - expect(page).to have_no_field "product_name", :with => "Product2" - end - - describe "clicking the 'Remove Filter' link" do - before(:each) do - find("a", text: "Remove Filter").click - end - - it "removes the filter from the list of applied filters" do - expect(page).to have_no_text "Name Equals Product1" - end - - it "loads appropriate products" do - expect(page).to have_field "product_name", :with => "Product1" - expect(page).to have_field "product_name", :with => "Product2" - end - end + # Products are hidden when filtered out + expect(page).to have_field "product_name", with: p1.name + expect(page).to have_no_field "product_name", with: p2.name end end end From 37ecadf6fc3e45877a57ee5a2d780e9bc5a4e489 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 6 Aug 2014 11:50:31 +1000 Subject: [PATCH 105/205] Taxons by ID converts id to string --- app/assets/javascripts/admin/services/taxons.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/services/taxons.js.coffee b/app/assets/javascripts/admin/services/taxons.js.coffee index a45770c787..46a973713c 100644 --- a/app/assets/javascripts/admin/services/taxons.js.coffee +++ b/app/assets/javascripts/admin/services/taxons.js.coffee @@ -4,7 +4,7 @@ angular.module("ofn.admin").factory "Taxons", (taxons, $filter) -> @taxons = taxons findByIDs: (ids) -> - taxon for taxon in @taxons when taxon.id in ids.split(",") + taxon for taxon in @taxons when taxon.id.toString() in ids.split(",") findByTerm: (term) -> $filter('filter')(@taxons, term) \ No newline at end of file From d27908fd2ff1dd1d0efeca6ba6de4df256ed926c Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 6 Aug 2014 13:27:17 +1000 Subject: [PATCH 106/205] Add ability to reset filters to BPE --- .../admin/bulk_product_update.js.coffee | 31 ++---- .../admin/filters/category_filter.js.coffee | 4 + .../admin/filters/producer_filter.js.coffee | 4 + .../spree/admin/products/bulk_edit.html.haml | 6 +- .../admin/bulk_product_update_spec.rb | 22 ++++- .../unit/bulk_product_update_spec.js.coffee | 97 ++----------------- 6 files changed, 46 insertions(+), 118 deletions(-) create mode 100644 app/assets/javascripts/admin/filters/category_filter.js.coffee create mode 100644 app/assets/javascripts/admin/filters/producer_filter.js.coffee diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 7206492c38..651cb18152 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -32,6 +32,10 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.producers = producers $scope.taxons = Taxons.taxons + $scope.filterProducers = [{id: "0", name: ""}].concat $scope.producers + $scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons + $scope.producerFilter = "0" + $scope.categoryFilter = "0" $scope.products = [] $scope.filteredProducts = [] $scope.currentFilters = [] @@ -148,29 +152,10 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ tab.visible = !tab.visible $scope.visibleTab = tab - $scope.addFilter = (filter) -> - existingfilterIndex = $scope.indexOfFilter filter - if $scope.filterableColumns.indexOf(filter.property) >= 0 && $scope.filterTypes.indexOf(filter.predicate) >= 0 && filter.value != "" && filter.value != undefined - if (DirtyProducts.count() > 0 and confirm("Unsaved changes will be lost. Continue anyway?")) or (DirtyProducts.count() == 0) - if existingfilterIndex == -1 - $scope.currentFilters.push filter - $scope.fetchProducts() - else if confirm("'#{filter.predicate.name}' filter already exists on column '#{filter.property.name}'. Replace it?") - $scope.currentFilters[existingfilterIndex] = filter - $scope.fetchProducts() - else - alert("Please ensure all filter fields are filled in before adding a filter.") - - $scope.removeFilter = (filter) -> - index = $scope.currentFilters.indexOf(filter) - if index != -1 - $scope.currentFilters.splice index, 1 - $scope.fetchProducts() - - $scope.indexOfFilter = (filter) -> - for existingFilter, i in $scope.currentFilters - return i if filter.property == existingFilter.property && filter.predicate == existingFilter.predicate - return -1 + $scope.resetSelectFilters = -> + $scope.query = "" + $scope.producerFilter = "0" + $scope.categoryFilter = "0" $scope.editWarn = (product, variant) -> if (DirtyProducts.count() > 0 and confirm("Unsaved changes will be lost. Continue anyway?")) or (DirtyProducts.count() == 0) diff --git a/app/assets/javascripts/admin/filters/category_filter.js.coffee b/app/assets/javascripts/admin/filters/category_filter.js.coffee new file mode 100644 index 0000000000..b89e706815 --- /dev/null +++ b/app/assets/javascripts/admin/filters/category_filter.js.coffee @@ -0,0 +1,4 @@ +angular.module("ofn.admin").filter "category", ($filter) -> + return (products, taxonID) -> + return products if taxonID == "0" + return $filter('filter')( products, { category: taxonID }, true ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/filters/producer_filter.js.coffee b/app/assets/javascripts/admin/filters/producer_filter.js.coffee new file mode 100644 index 0000000000..7325b2200d --- /dev/null +++ b/app/assets/javascripts/admin/filters/producer_filter.js.coffee @@ -0,0 +1,4 @@ +angular.module("ofn.admin").filter "producer", ($filter) -> + return (products, producerID) -> + return products if producerID == "0" + $filter('filter')( products, { producer: producerID }, true ) \ No newline at end of file diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index f00ed1c7c1..cb51b5c3b6 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -29,11 +29,11 @@ .filter_select{ :class => "three columns" } %label{ :for => 'producer_filter' }Producer %br - %select{ :class => "three columns alpha", :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in producers' } + %select{ :class => "three columns alpha", :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in filterProducers' } .filter_select{ :class => "three columns" } %label{ :for => 'taxon_filter' }Category %br - %select{ :class => "three columns alpha", :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in taxons'} + %select{ :class => "three columns alpha", :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in filterTaxons'} .filter_clear{ :class => "two columns omega" } %label{ :for => 'clear_all_filters' } %br @@ -78,7 +78,7 @@ %th.actions %th.actions %th.actions - %tbody{ 'ng-repeat' => 'product in filteredProducts = ( products | filter:query | filter:{producer: producerFilter}:true | filter:{category: categoryFilter}:true | limitTo:limit )', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } + %tbody{ 'ng-repeat' => 'product in filteredProducts = ( products | filter:query | producer: producerFilter | category: categoryFilter | limitTo:limit )', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } %tr.product{ :id => "p_{{product.id}}" } %td.left-actions %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' } diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index bc2e0d7b49..5beac13e0e 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -555,7 +555,9 @@ feature %q{ expect(page).to have_selector "a.delete-product", :count => 3 - first("a.delete-product").click + within "tr#p_#{p1.id}" do + first("a.delete-product").click + end expect(page).to have_selector "a.delete-product", :count => 2 @@ -576,7 +578,9 @@ feature %q{ expect(page).to have_selector "a.delete-variant", :count => 3 - first("a.delete-variant").click + within "tr#v_#{v1.id}" do + first("a.delete-variant").click + end expect(page).to have_selector "a.delete-variant", :count => 2 @@ -599,7 +603,9 @@ feature %q{ expect(page).to have_selector "a.edit-product", :count => 3 - first("a.edit-product").click + within "tr#p_#{p1.id}" do + first("a.edit-product").click + end URI.parse(current_url).path.should == "/admin/products/#{p1.permalink}/edit" end @@ -668,7 +674,7 @@ feature %q{ expect(page).to have_selector "th", :text => "ON HAND" expect(page).to have_selector "th", :text => "AV. ON" - first("div#columns_dropdown div.menu div.menu_item", text: /.{0,1}Producer/).click + first("div#columns_dropdown div.menu div.menu_item", text: /^.{0,1}Producer$/).click expect(page).to have_no_selector "th", :text => "PRODUCER" expect(page).to have_selector "th", :text => "NAME" @@ -696,11 +702,19 @@ feature %q{ expect(page).to have_field "product_name", with: p1.name expect(page).to have_field "product_name", with: p2.name + # Set a filter select2_select s1.name, from: "producer_filter" # Products are hidden when filtered out expect(page).to have_field "product_name", with: p1.name expect(page).to have_no_field "product_name", with: p2.name + + # Clearing filters + click_button "Clear All" + + # All products are shown again + expect(page).to have_field "product_name", with: p1.name + expect(page).to have_field "product_name", with: p2.name end end end diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 2543d5c0e8..270af2aaeb 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -1094,94 +1094,15 @@ describe "AdminProductEditCtrl", -> describe "filtering products", -> - describe "adding a filter to the filter list", -> - filterObject1 = filterObject2 = null - - beforeEach -> - spyOn($scope, "fetchProducts").andReturn "nothing" - spyOn(DirtyProducts, "count").andReturn 0 - filterObject1 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[0], value: "value1"} - filterObject2 = {property: $scope.filterableColumns[1], predicate: $scope.filterTypes[1], value: "value2"} - $scope.addFilter filterObject1 - $scope.addFilter filterObject2 - - it "adds objects sent to addFilter() to $scope.currentFilters", -> - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "ignores objects sent to addFilter() which do not contain a 'property' with a corresponding key in filterableColumns", -> - filterObject3 = {property: "some_random_property", predicate: $scope.filterTypes[0], value: "value3"} - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "ignores objects sent to addFilter() which do not contain a query with a corresponding key in filterTypes", -> - filterObject3 = {property: $scope.filterableColumns[0], predicate: "something", value: "value3"} - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "ignores objects sent to addFilter() which have a blank 'value' property", -> - filterObject3 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[1], value: ""} - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "calls fetchProducts when adding a new filter", -> - expect($scope.fetchProducts.calls.length).toEqual(2) - - describe "when unsaved products exist", -> - beforeEach -> - filterObject3 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[1], value: "value3"} - spyOn(window, "confirm").andReturn false - DirtyProducts.count.andReturn 1 - $scope.addFilter filterObject3 - - it "it does not call fetchProducts", -> - expect($scope.fetchProducts.calls.length).toEqual(2) - - it "does not add the filter to $scope.currentFilters", -> - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "asks the user to save changes before proceeding", -> - expect(window.confirm).toHaveBeenCalledWith "Unsaved changes will be lost. Continue anyway?" - - describe "when a filter on the same property and predicate already exists", -> - filterObject3 = null - - beforeEach -> - filterObject3 = { property: filterObject2.property, predicate: filterObject2.predicate, value: "new value" } - - it "asks the user for permission before proceeding", -> - spyOn(window, "confirm").andReturn true - $scope.addFilter filterObject3 - expect(window.confirm).toHaveBeenCalledWith "'#{filterObject3.predicate.name}' filter already exists on column '#{filterObject3.property.name}'. Replace it?" - - it "replaces the filter in $scope.currentFilters when user clicks OK", -> - spyOn(window, "confirm").andReturn true - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject3] - - it "does not add the filter to $scope.currentFilters when user clicks cancel", -> - spyOn(window, "confirm").andReturn false - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - describe "removing a filter from the filter list", -> - filterObject1 = filterObject2 = null - - beforeEach -> - spyOn($scope, "fetchProducts").andReturn "nothing" - filterObject1 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[0], value: "Product1"} - filterObject2 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[0], value: "Product2"} - $scope.currentFilters = [ filterObject1, filterObject2 ] - - it "removes the specified filter from $scope.currentFilters and calls fetchProducts", -> - $scope.removeFilter filterObject1 - expect($scope.currentFilters).toEqual [ filterObject2 ] - expect($scope.fetchProducts.calls.length).toEqual 1 - - it "ignores filters which do not exist in currentFilters", -> - filterObject3 = {property: $scope.filterableColumns[1], predicate: $scope.filterTypes[1], value: "SomethingElse"} - $scope.removeFilter filterObject3 - expect($scope.currentFilters).toEqual [ filterObject1, filterObject2 ] - expect($scope.fetchProducts.calls.length).toEqual 0 + describe "clearing filters", -> + it "resets filter variables", -> + $scope.query = "lala" + $scope.producerFilter = "5" + $scope.categoryFilter = "6" + $scope.resetSelectFilters() + expect($scope.query).toBe "" + expect($scope.producerFilter).toBe "0" + expect($scope.categoryFilter).toBe "0" describe "converting arrays of objects with ids to an object with ids as keys", -> From 1e680527112aa79f0765c9fcbbdae8ce4d7a1c90 Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 7 Aug 2014 15:27:32 +1000 Subject: [PATCH 107/205] Moving update button to top of BPE --- .../admin/bulk_product_update.js.coffee | 6 +- .../stylesheets/admin/products.css.scss | 82 +------------------ .../spree/admin/products/bulk_edit.html.haml | 51 ++++++------ 3 files changed, 32 insertions(+), 107 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 651cb18152..155835a979 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -322,19 +322,19 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.displayUpdating = -> - $scope.setMessage $scope.updateStatusMessage, "Updating...", + $scope.setMessage $scope.updateStatusMessage, "Saving...", color: "orange" , false $scope.displaySuccess = -> - $scope.setMessage $scope.updateStatusMessage, "Update complete", + $scope.setMessage $scope.updateStatusMessage, "Changes Saved.", color: "green" , 3000 $scope.displayFailure = (failMessage) -> - $scope.setMessage $scope.updateStatusMessage, "Updating failed. " + failMessage, + $scope.setMessage $scope.updateStatusMessage, "Saving failed. " + failMessage, color: "red" , 10000 diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index c8ceaaaac2..9071aee41e 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -2,59 +2,6 @@ display: block; } -div.pagination { - div.pagenav { - margin: 0px; - span.first, span.prev, span.next, span.last { - padding: 5px 0px; - display:inline-block; - } - } -} - -div.pagination_info { - text-align: right; -} - - - -div.applied_filter { - margin-bottom: 5px; - border: solid 2px #5498da; - padding: 5px 0px; - border-radius: 5px; - div.four.columns { - padding-left: 10px; - } -} - -div.option_tabs { - div.applied_filters, div.filters, div.column_toggle { - margin-bottom: 10px; - } -} - -div.option_tab_titles { - h6 { - border-radius: 3px; - border: 1px solid #cee1f4; - padding: 3px; - text-align: center; - color: darken(#cee1f4, 3); - cursor: pointer; - -moz-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - } - h6.selected { - border: 1px solid #5498da; - color: #5498da; - } - margin-bottom: 10px; -} - tbody.odd { tr.product { td { background-color: white; } } tr.variant.odd { td { background-color: lighten(#eff5fc, 3); } } @@ -76,33 +23,8 @@ th.left-actions, td.left-actions { border-right: 1px solid #cee1f4 !important; } -li.column-list-item { - border-radius: 3px; - padding: 2px 20px; - margin: 2px 1px; - background-color: white; - border: 2px solid lightgray; - color: darkgray; - font-size: 100%; - cursor: default; - text-align: center; - cursor: pointer; - -moz-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -li.column-list-item.selected { - border: 2px solid #5498da; - background-color: #5498da; - color: white; - font-size: 100%; -} - -ul.column-list { - list-style: none; +#update-status-message { + margin: 6px 0px; } table#listing_products.bulk { diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index cb51b5c3b6..4101109267 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -15,33 +15,38 @@ =admin_inject_producers =admin_inject_taxons %div{ 'ng-app' => 'ofn.admin', 'ng-controller' => 'AdminProductEditCtrl', 'ng-init' => "initialise('#{@spree_api_key}');loading=true;" } + %div.sixteen.columns.alpha + %div.quick_search{ :class => "four columns alpha" } + %label{ :for => 'quick_filter' } + %br + %input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } + .filter_select{ :class => "four columns" } + %label{ :for => 'producer_filter' }Producer + %br + %select{ :class => "four columns alpha", :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in filterProducers' } + .filter_select{ :class => "four columns" } + %label{ :for => 'taxon_filter' }Category + %br + %select{ :class => "four columns alpha", :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in filterTaxons'} + %div{ :class => "one column" }   + .filter_clear{ :class => "three columns omega" } + %label{ :for => 'clear_all_filters' } + %br + %input.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "Clear Filters", 'ng-click' => "resetSelectFilters()" } + %hr.sixteen.columns.alpha %div{ 'ng-show' => '!spree_api_key_ok' } {{ api_error_msg }} %div.sixteen.columns.alpha.loading{ 'ng-show' => 'loading' } %h4 Loading Products... %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length == 0' } %h4{ :style => 'color:red;' } No matching products found. - %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0' } - %div.quick_search{ :class => "four columns alpha" } - %label{ :for => 'quick_filter' } - %br - %input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } - .filter_select{ :class => "three columns" } - %label{ :for => 'producer_filter' }Producer - %br - %select{ :class => "three columns alpha", :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in filterProducers' } - .filter_select{ :class => "three columns" } - %label{ :for => 'taxon_filter' }Category - %br - %select{ :class => "three columns alpha", :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in filterTaxons'} - .filter_clear{ :class => "two columns omega" } - %label{ :for => 'clear_all_filters' } - %br - %input.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "Clear All", 'ng-click' => "resetSelectFilters()" } - %div{ :class => "one column" }   - %div{ :class => "three columns omega" } - %label{ } - %br + %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0', style: "margin-bottom: 10px" } + %div.four.columns.alpha + %input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'} + %div.nine.columns + %div{ id: "update-status-message", ng: { style: 'updateStatusMessage.style' } } + {{ updateStatusMessage.text || " " }} + %div.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'" } @@ -49,6 +54,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 }} + %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0' } %table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" } %colgroup %col.actions @@ -132,6 +138,3 @@ %td.actions %td.actions %a{ 'ng-click' => 'deleteVariant(product,variant)', :class => "delete-variant icon-trash no-text" } - %input{ :type => 'button', :value => 'Update', 'ng-click' => 'submitProducts()'} - %span{ id: "update-status-message", 'ng-style' => 'updateStatusMessage.style' } - {{ updateStatusMessage.text }} From 32c8b9cdcd2126a94cb4b1925e248d9a5dadb6fe Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 8 Aug 2014 10:54:10 +1000 Subject: [PATCH 108/205] Switch Taxons input over to category selection on BPE, clean up interface --- .../admin/bulk_product_update.js.coffee | 8 +- .../directives/taxon_autocomplete.js.coffee | 6 +- .../admin/services/taxons.js.coffee | 5 + .../stylesheets/admin/products.css.scss | 30 +++- .../spree/api/product_serializer.rb | 6 +- .../spree/admin/products/bulk_edit.html.haml | 42 ++--- .../admin/bulk_product_update_spec.rb | 143 ++++++++++-------- .../unit/admin/services/taxons_spec.js.coffee | 9 +- 8 files changed, 144 insertions(+), 105 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 155835a979..1f652a2264 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -11,7 +11,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ unit: {name: "Unit", visible: true} price: {name: "Price", visible: true} on_hand: {name: "On Hand", visible: true} - taxons: {name: "Taxons", visible: false} + category: {name: "Category", visible: false} available_on: {name: "Available On", visible: false} $scope.variant_unit_options = VariantUnitManager.variantUnitOptions() @@ -328,7 +328,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.displaySuccess = -> - $scope.setMessage $scope.updateStatusMessage, "Changes Saved.", + $scope.setMessage $scope.updateStatusMessage, "Changes saved.", color: "green" , 3000 @@ -394,8 +394,8 @@ filterSubmitProducts = (productsToFilter) -> if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present filteredProduct.on_hand = product.on_hand hasUpdatableProperty = true - if product.hasOwnProperty("taxon_ids") - filteredProduct.taxon_ids = product.taxon_ids + if product.hasOwnProperty("category") + filteredProduct.primary_taxon_id = product.category hasUpdatableProperty = true if product.hasOwnProperty("available_on") filteredProduct.available_on = product.available_on diff --git a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee index 0a1c01d38f..5f17a0d1dd 100644 --- a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee @@ -4,10 +4,10 @@ angular.module("ofn.admin").directive "ofnTaxonAutocomplete", (Taxons) -> link: (scope,element,attrs,ngModel) -> setTimeout -> element.select2 - placeholder: Spree.translations.taxon_placeholder - multiple: true + placeholder: "Category" + multiple: false initSelection: (element, callback) -> - callback Taxons.findByIDs(element.val()) + callback Taxons.findByID(scope.product.category) query: (query) -> query.callback { results: Taxons.findByTerm(query.term) } formatResult: (taxon) -> diff --git a/app/assets/javascripts/admin/services/taxons.js.coffee b/app/assets/javascripts/admin/services/taxons.js.coffee index 46a973713c..6944fe132f 100644 --- a/app/assets/javascripts/admin/services/taxons.js.coffee +++ b/app/assets/javascripts/admin/services/taxons.js.coffee @@ -3,6 +3,11 @@ angular.module("ofn.admin").factory "Taxons", (taxons, $filter) -> constructor: -> @taxons = taxons + # For finding a single Taxon + findByID: (id) -> + $filter('filter')(@taxons, {id: id}, true)[0] + + # For finding multiple Taxons represented by comma delimited string findByIDs: (ids) -> taxon for taxon in @taxons when taxon.id.toString() in ids.split(",") diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index 9071aee41e..17e9af224e 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -30,9 +30,33 @@ th.left-actions, td.left-actions { table#listing_products.bulk { clear: both; - td.supplier { - select { - width: 125px; + colgroup col { + &.producer { + width: 18%; + } + &.name { + width: 18%; + } + &.unit { + width: 14%; + } + &.display_as { + width: 12%; + } + &.price { + width: 10%; + } + &.on_hand { + width: 10%; + } + &.category { + width: 15%; + } + &.available_on { + width: 15%; + } + &.actions { + width: 3%; } } diff --git a/app/serializers/spree/api/product_serializer.rb b/app/serializers/spree/api/product_serializer.rb index cc48b30929..aa8919f8d6 100644 --- a/app/serializers/spree/api/product_serializer.rb +++ b/app/serializers/spree/api/product_serializer.rb @@ -1,17 +1,13 @@ class Spree::Api::ProductSerializer < ActiveModel::Serializer attributes :id, :name, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand - attributes :taxon_ids, :on_hand, :price, :available_on, :permalink_live + attributes :on_hand, :price, :available_on, :permalink_live has_one :supplier, key: :producer, embed: :id has_one :primary_taxon, key: :category, embed: :id has_many :variants, key: :variants, serializer: Spree::Api::VariantSerializer # embed: ids has_one :master, serializer: Spree::Api::VariantSerializer - def taxon_ids - object.taxons.map{ |t| t.id }.join(",") - end - def on_hand object.on_hand.nil? ? 0 : object.on_hand.to_f.finite? ? object.on_hand : "On demand" end diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index 4101109267..75c5040a3d 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -25,7 +25,7 @@ %br %select{ :class => "four columns alpha", :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in filterProducers' } .filter_select{ :class => "four columns" } - %label{ :for => 'taxon_filter' }Category + %label{ :for => 'category_filter' }Category %br %select{ :class => "four columns alpha", :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in filterTaxons'} %div{ :class => "one column" }   @@ -34,12 +34,6 @@ %br %input.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "Clear Filters", 'ng-click' => "resetSelectFilters()" } %hr.sixteen.columns.alpha - %div{ 'ng-show' => '!spree_api_key_ok' } - {{ api_error_msg }} - %div.sixteen.columns.alpha.loading{ 'ng-show' => 'loading' } - %h4 Loading Products... - %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length == 0' } - %h4{ :style => 'color:red;' } No matching products found. %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0', style: "margin-bottom: 10px" } %div.four.columns.alpha %input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'} @@ -54,18 +48,24 @@ %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 }} - %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0' } + %div{ 'ng-show' => '!spree_api_key_ok' } + {{ api_error_msg }} + %div.sixteen.columns.alpha.loading{ 'ng-show' => 'loading' } + %h4 Loading Products... + %div.sixteen.columns.alpha{ 'ng-show' => '!loading && filteredProducts.length == 0' } + %h4{ :style => 'color:red;' } No products found. + %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || filteredProducts.length == 0' } %table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" } %colgroup %col.actions - %col.producer{ 'style' => 'width: 14%;', 'ng-show' => 'columns.producer.visible' } - %col.name{ 'style' => 'width: 20%;', 'ng-show' => 'columns.name.visible' } - %col.unit{ 'style' => 'width: 14%;', 'ng-show' => 'columns.unit.visible' } - %col.display_as{ 'style' => 'width: 12%;', 'ng-show' => 'columns.unit.visible' } - %col.price{ 'style' => 'width: 10%;', 'ng-show' => 'columns.price.visible'} - %col.on_hand{ 'style' => 'width: 10%;', 'ng-show' => 'columns.on_hand.visible' } - %col.taxons{ 'ng-show' => 'columns.taxons.visible' } - %col.available_on{ 'ng-show' => 'columns.available_on.visible' } + %col.producer{ ng: { show: 'columns.producer.visible' } } + %col.name{ ng: { show: 'columns.name.visible' } } + %col.unit{ ng: { show: 'columns.unit.visible' } } + %col.display_as{ ng: { show: 'columns.unit.visible' } } + %col.price{ ng: { show: 'columns.price.visible'} } + %col.on_hand{ ng: { show: 'columns.on_hand.visible' } } + %col.category{ ng: { show: 'columns.category.visible' } } + %col.available_on{ ng: { show: 'columns.available_on.visible' } } %col.actions %col.actions %col.actions @@ -79,7 +79,7 @@ %th.display_as{ 'ng-show' => 'columns.unit.visible' } Display As %th.price{ 'ng-show' => 'columns.price.visible' } Price %th.on_hand{ 'ng-show' => 'columns.on_hand.visible' } On Hand - %th.taxons{ 'ng-show' => 'columns.taxons.visible' } Taxons + %th.category{ 'ng-show' => 'columns.category.visible' } Category %th.available_on{ 'ng-show' => 'columns.available_on.visible' } Av. On %th.actions %th.actions @@ -90,7 +90,7 @@ %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", '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{ 'ng-model' => 'product.producer', :name => 'producer', 'ofn-track-product' => 'producer', 'ng-options' => 'producer.id as producer.name for producer in producers' } + %select.select2.fullwidth{ 'ng-model' => 'product.producer', :name => 'producer', 'ofn-track-product' => 'producer', 'ng-options' => 'producer.id as producer.name for producer in producers' } %td.name{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' } %td.unit{ 'ng-show' => 'columns.unit.visible' } @@ -105,8 +105,8 @@ %td.on_hand{ 'ng-show' => 'columns.on_hand.visible' } %span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' } %input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-hide' => 'hasVariants(product) || product.on_demand', :type => 'number' } - %td{ 'ng-if' => 'columns.taxons.visible' } - %input.fullwidth{ :type => 'text', 'ng-model' => 'product.taxon_ids', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'taxon_ids' } + %td.category{ 'ng-if' => 'columns.category.visible' } + %input.fullwidth{ :type => 'text', id: "p{{product.id}}_category", 'ng-model' => 'product.category', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category' } %td.available_on{ 'ng-show' => 'columns.available_on.visible' } %input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ofn-track-product' => 'available_on', 'datetimepicker' => 'product.available_on', type: "text" } %td.actions @@ -131,7 +131,7 @@ %td{ 'ng-show' => 'columns.on_hand.visible' } %input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ng-hide' => 'variant.on_demand', 'ofn-track-variant' => 'on_hand', :type => 'number' } %span{ 'ng-bind' => 'variant.on_hand', :name => 'variant_on_hand', 'ng-show' => 'variant.on_demand' } - %td{ 'ng-show' => 'columns.taxons.visible' } + %td{ 'ng-show' => 'columns.category.visible' } %td{ 'ng-show' => 'columns.available_on.visible' } %td.actions %a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text", 'ng-show' => "variantSaved(variant)" } diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 5beac13e0e..11c82eed58 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -25,7 +25,7 @@ feature %q{ it "displays a message when number of products is zero" do visit '/admin/products/bulk_edit' - expect(page).to have_text "No matching products found." + expect(page).to have_text "No products found." end it "displays a select box for suppliers, with the appropriate supplier selected" do @@ -266,8 +266,8 @@ feature %q{ fill_in "variant_display_as", with: "Case" fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" - click_button 'Update' - expect(page.find("span#update-status-message")).to have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." updated_variant = Spree::Variant.where(deleted_at: nil).last expect(updated_variant.display_name).to eq "Case of 12 Bottles" @@ -285,7 +285,9 @@ feature %q{ scenario "updating a product with no variants (except master)" do s1 = FactoryGirl.create(:supplier_enterprise) s2 = FactoryGirl.create(:supplier_enterprise) - p = FactoryGirl.create(:product, supplier: s1, available_on: Date.today, variant_unit: 'volume', variant_unit_scale: 1) + t1 = FactoryGirl.create(:taxon) + t2 = FactoryGirl.create(:taxon) + p = FactoryGirl.create(:product, supplier: s1, available_on: Date.today, variant_unit: 'volume', variant_unit_scale: 1, primary_taxon: t2) p.price = 10.0 p.on_hand = 6; p.save! @@ -296,24 +298,30 @@ feature %q{ first("div#columns_dropdown", :text => "COLUMNS").click first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click + first("div#columns_dropdown div.menu div.menu_item", text: "Category").click - expect(page).to have_field "product_name", with: p.name - expect(page).to have_select "producer", selected: s1.name - expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") - expect(page).to have_field "price", with: "10.0" - expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)" - expect(page).to have_field "on_hand", with: "6" + within "tr#p_#{p.id}" do + expect(page).to have_field "product_name", with: p.name + expect(page).to have_select "producer", selected: s1.name + expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") + expect(page).to have_field "price", with: "10.0" + save_screenshot '/Users/rob/Desktop/ss.png' + expect(page).to have_selector "div#s2id_p#{p.id}_category a.select2-choice" + expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)" + expect(page).to have_field "on_hand", with: "6" - fill_in "product_name", with: "Big Bag Of Potatoes" - select(s2.name, :from => 'producer') - fill_in "available_on", with: (Date.today-3).strftime("%F %T") - fill_in "price", with: "20" - select "Weight (kg)", from: "variant_unit_with_scale" - fill_in "on_hand", with: "18" - fill_in "display_as", with: "Big Bag" + fill_in "product_name", with: "Big Bag Of Potatoes" + select s2.name, :from => 'producer' + fill_in "available_on", with: (Date.today-3).strftime("%F %T") + fill_in "price", with: "20" + select "Weight (kg)", from: "variant_unit_with_scale" + select2_select t1.name, from: "p#{p.id}_category" + fill_in "on_hand", with: "18" + fill_in "display_as", with: "Big Bag" + end - click_button 'Update' - expect(page.find("span#update-status-message")).to have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "Big Bag Of Potatoes" @@ -324,6 +332,7 @@ feature %q{ expect(p.master.display_as).to eq "Big Bag" expect(p.price).to eq 20.0 expect(p.on_hand).to eq 18 + expect(p.primary_taxon).to eq t1 end scenario "updating a product with a variant unit of 'items'" do @@ -338,13 +347,13 @@ feature %q{ select "Items", from: "variant_unit_with_scale" fill_in "variant_unit_name", with: "loaf" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p.reload - p.variant_unit.should == "items" - p.variant_unit_scale.should be_nil - p.variant_unit_name.should == "loaf" + expect(p.variant_unit).to eq "items" + expect(p.variant_unit_scale).to be_nil + expect(p.variant_unit_name).to eq "loaf" end scenario "setting a variant unit on a product that has none" do @@ -361,15 +370,15 @@ feature %q{ first("a.view-variants").trigger('click') fill_in "variant_unit_value_with_description", with: '123 abc' - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p.reload - p.variant_unit.should == "weight" - p.variant_unit_scale.should == 1000 # Kg + expect(p.variant_unit).to eq "weight" + expect(p.variant_unit_scale).to eq 1000 # Kg v.reload - v.unit_value.should == 123000 # 123 kg in g - v.unit_description.should == "abc" + expect(v.unit_value).to eq 123000 # 123 kg in g + expect(v.unit_description).to eq "abc" end describe "setting the master unit value for a product without variants" do @@ -386,14 +395,14 @@ feature %q{ select "Weight (kg)", from: "variant_unit_with_scale" fill_in "master_unit_value_with_description", with: '123 abc' - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p.reload - p.variant_unit.should == 'weight' - p.variant_unit_scale.should == 1000 - p.master.unit_value.should == 123000 - p.master.unit_description.should == 'abc' + expect(p.variant_unit).to eq 'weight' + expect(p.variant_unit_scale).to eq 1000 + expect(p.master.unit_value).to eq 123000 + expect(p.master.unit_description).to eq 'abc' end it "does not show the field when the product has variants" do @@ -434,14 +443,14 @@ feature %q{ expect(page).to have_selector "span[name='on_hand']", text: "10" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." v.reload - v.price.should == 4.0 - v.on_hand.should == 10 - v.unit_value.should == 2 # 2L in L - v.unit_description.should == "(8x250 mL bottles)" + expect(v.price).to eq 4.0 + expect(v.on_hand).to eq 10 + expect(v.unit_value).to eq 2 # 2L in L + expect(v.unit_description).to eq "(8x250 mL bottles)" end scenario "updating delegated attributes of variants in isolation" do @@ -458,11 +467,11 @@ feature %q{ fill_in "variant_price", with: "10.0" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." v.reload - v.price.should == 10.0 + expect(v.price).to eq 10.0 end scenario "updating a product mutiple times without refresh" do @@ -475,24 +484,24 @@ feature %q{ fill_in "product_name", with: "new name 1" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p.reload - p.name.should == "new name 1" + expect(p.name).to eq "new name 1" fill_in "product_name", with: "new name 2" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p.reload - p.name.should == "new name 2" + expect(p.name).to eq "new name 2" fill_in "product_name", with: "original name" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p.reload - p.name.should == "original name" + expect(p.name).to eq "original name" end scenario "updating a product after cloning a product" do @@ -505,10 +514,10 @@ feature %q{ fill_in "product_name", :with => "new product name" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p.reload - p.name.should == "new product name" + expect(p.name).to eq "new product name" end scenario "updating when no changes have been made" do @@ -518,8 +527,8 @@ feature %q{ visit '/admin/products/bulk_edit' - click_button 'Update' - page.find("span#update-status-message").should have_content "No changes to update." + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "No changes to update." end end @@ -537,10 +546,10 @@ feature %q{ expect(page).to have_no_field "product_name", with: p2.name fill_in "product_name", :with => "new product1" - click_on 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p1.reload - p1.name.should == "new product1" + expect(p1.name).to eq "new product1" end describe "using action buttons" do @@ -607,7 +616,7 @@ feature %q{ first("a.edit-product").click end - URI.parse(current_url).path.should == "/admin/products/#{p1.permalink}/edit" + expect(URI.parse(current_url).path).to eq "/admin/products/#{p1.permalink}/edit" end it "shows an edit button for variants, which takes the user to the standard edit page for that variant" do @@ -626,7 +635,7 @@ feature %q{ first("a.edit-variant").click end - URI.parse(current_url).path.should == "/admin/products/#{v1.product.permalink}/variants/#{v1.id}/edit" + expect(URI.parse(current_url).path).to eq "/admin/products/#{v1.product.permalink}/variants/#{v1.id}/edit" end end @@ -710,7 +719,7 @@ feature %q{ expect(page).to have_no_field "product_name", with: p2.name # Clearing filters - click_button "Clear All" + click_button "Clear Filters" # All products are shown again expect(page).to have_field "product_name", with: p1.name @@ -781,8 +790,8 @@ feature %q{ fill_in "on_hand", with: "18" fill_in "display_as", with: "Big Bag" - click_button 'Update' - expect(page.find("span#update-status-message")).to have_content "Update complete" + click_button 'Save Changes' + expect(page.find("div#update-status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "Big Bag Of Potatoes" diff --git a/spec/javascripts/unit/admin/services/taxons_spec.js.coffee b/spec/javascripts/unit/admin/services/taxons_spec.js.coffee index 4c69052b38..9dd22935d5 100644 --- a/spec/javascripts/unit/admin/services/taxons_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/taxons_spec.js.coffee @@ -12,7 +12,12 @@ describe "Taxons service", -> $resource = _$resource_ $httpBackend = _$httpBackend_ - describe "calling findByIDs", -> + describe "findByID", -> + it "returns the taxon with exactly matching id, ignoring ids which do not exactly match", -> + result = Taxons.findByID("1") + expect(result).toEqual {id: "1", name: "t1"} + + describe "findByIDs", -> it "returns taxons with exactly matching ids", -> result = Taxons.findByIDs("1,2") expect(result).toEqual [{id: "1", name: "t1"}, {id: "2", name: "t2"}] @@ -21,7 +26,7 @@ describe "Taxons service", -> result = Taxons.findByIDs("1,3") expect(result).toEqual [{id: "1", name: "t1"}] - describe "calling findByTerm", -> + describe "findByTerm", -> it "returns taxons which match partially", -> result = Taxons.findByTerm("t1") expect(result).toEqual [{id: "1", name: "t1"}, {id: "12", name: "t12"}] \ No newline at end of file From e6f7f26a954026c14204673c3100a4c3bac00f00 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 8 Aug 2014 12:02:45 +1000 Subject: [PATCH 109/205] Add loading gif to BPE, fiddle with interface --- .../admin/bulk_product_update.js.coffee | 11 ++++---- .../stylesheets/admin/products.css.scss | 20 ++++++++++++- .../spree/admin/products/bulk_edit.html.haml | 11 ++++---- .../admin/bulk_product_update_spec.rb | 28 +++++++++---------- 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 1f652a2264..1b0ec47698 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -255,7 +255,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ if productsToSubmit.length > 0 $scope.updateProducts productsToSubmit # Don't submit an empty list else - $scope.setMessage $scope.updateStatusMessage, "No changes to update.", color: "grey", 3000 + $scope.setMessage $scope.updateStatusMessage, "No changes to save.", color: "grey", 3000 $scope.updateProducts = (productsToSubmit) -> @@ -323,25 +323,26 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.displayUpdating = -> $scope.setMessage $scope.updateStatusMessage, "Saving...", - color: "orange" + color: "#FF9906" , false $scope.displaySuccess = -> $scope.setMessage $scope.updateStatusMessage, "Changes saved.", - color: "green" + color: "#9fc820" , 3000 $scope.displayFailure = (failMessage) -> $scope.setMessage $scope.updateStatusMessage, "Saving failed. " + failMessage, - color: "red" + color: "#DA5354" , 10000 $scope.displayDirtyProducts = -> if DirtyProducts.count() > 0 - $scope.setMessage $scope.updateStatusMessage, "Changes to " + DirtyProducts.count() + " products remain unsaved.", + message = if DirtyProducts.count() == 1 then "one product" else DirtyProducts.count() + " products" + $scope.setMessage $scope.updateStatusMessage, "Changes to " + message + " remain unsaved.", color: "gray" , false else diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index 17e9af224e..2fe37d1e14 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -24,7 +24,25 @@ th.left-actions, td.left-actions { } #update-status-message { - margin: 6px 0px; + margin: 4px 0px; + font-weight: bold; +} + +#no_products { + font-weight:bold; + color: #DA5354; +} + +#loading { + text-align: center; + img.spinner { + width: 100px; + height: 100px; + } + h1 { + margin-top: 20px; + color: gray; + } } table#listing_products.bulk { diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index 75c5040a3d..522fcf9458 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -38,10 +38,10 @@ %div.four.columns.alpha %input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'} %div.nine.columns - %div{ id: "update-status-message", ng: { style: 'updateStatusMessage.style' } } + %h6{ id: "update-status-message", ng: { style: 'updateStatusMessage.style' } } {{ updateStatusMessage.text || " " }} %div.three.columns.omega - %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' } + %div.ofn_drop_down.three.columns.omega{ '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" } @@ -50,10 +50,11 @@ %span{ :class => 'two columns omega' } {{column.name }} %div{ 'ng-show' => '!spree_api_key_ok' } {{ api_error_msg }} - %div.sixteen.columns.alpha.loading{ 'ng-show' => 'loading' } - %h4 Loading Products... + %div.sixteen.columns.alpha#loading{ 'ng-if' => 'loading' } + %img.spinner{ src: "/assets/loading.gif" } + %h1 LOADING PRODUCTS %div.sixteen.columns.alpha{ 'ng-show' => '!loading && filteredProducts.length == 0' } - %h4{ :style => 'color:red;' } No products found. + %h1#no_products No products found. %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || filteredProducts.length == 0' } %table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" } %colgroup diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 11c82eed58..ccddba5508 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -267,7 +267,7 @@ feature %q{ fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." updated_variant = Spree::Variant.where(deleted_at: nil).last expect(updated_variant.display_name).to eq "Case of 12 Bottles" @@ -321,7 +321,7 @@ feature %q{ end click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "Big Bag Of Potatoes" @@ -348,7 +348,7 @@ feature %q{ fill_in "variant_unit_name", with: "loaf" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload expect(p.variant_unit).to eq "items" @@ -371,7 +371,7 @@ feature %q{ fill_in "variant_unit_value_with_description", with: '123 abc' click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload expect(p.variant_unit).to eq "weight" @@ -396,7 +396,7 @@ feature %q{ fill_in "master_unit_value_with_description", with: '123 abc' click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload expect(p.variant_unit).to eq 'weight' @@ -444,7 +444,7 @@ feature %q{ expect(page).to have_selector "span[name='on_hand']", text: "10" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." v.reload expect(v.price).to eq 4.0 @@ -468,7 +468,7 @@ feature %q{ fill_in "variant_price", with: "10.0" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." v.reload expect(v.price).to eq 10.0 @@ -485,21 +485,21 @@ feature %q{ fill_in "product_name", with: "new name 1" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "new name 1" fill_in "product_name", with: "new name 2" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "new name 2" fill_in "product_name", with: "original name" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "original name" end @@ -515,7 +515,7 @@ feature %q{ fill_in "product_name", :with => "new product name" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "new product name" end @@ -528,7 +528,7 @@ feature %q{ visit '/admin/products/bulk_edit' click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "No changes to update." + expect(page.find("#update-status-message")).to have_content "No changes to save." end end @@ -547,7 +547,7 @@ feature %q{ fill_in "product_name", :with => "new product1" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p1.reload expect(p1.name).to eq "new product1" end @@ -791,7 +791,7 @@ feature %q{ fill_in "display_as", with: "Big Bag" click_button 'Save Changes' - expect(page.find("div#update-status-message")).to have_content "Changes saved." + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "Big Bag Of Potatoes" From 3b9cd3f46a7348a21ecd3385ebe6c8d6bf734147 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 8 Aug 2014 12:36:30 +1000 Subject: [PATCH 110/205] Make loading on BOM look more like BPE --- .../stylesheets/admin/openfoodnetwork.css.scss | 18 ++++++++++++++++++ app/assets/stylesheets/admin/products.css.scss | 17 ----------------- .../admin/orders/bulk_management.html.haml | 7 ++++--- .../spree/admin/products/bulk_edit.html.haml | 2 +- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 87983a6bc7..3281c8d116 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -140,6 +140,24 @@ table#listing_enterprise_groups { } } +#no_results { + font-weight:bold; + color: #DA5354; +} + + +#loading { + text-align: center; + img.spinner { + width: 100px; + height: 100px; + } + h1 { + margin-top: 20px; + color: gray; + } +} + .ofn_drop_down { padding: 7px 15px; border-radius: 3px; diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index 2fe37d1e14..bddc7f9e9e 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -28,23 +28,6 @@ th.left-actions, td.left-actions { font-weight: bold; } -#no_products { - font-weight:bold; - color: #DA5354; -} - -#loading { - text-align: center; - img.spinner { - width: 100px; - height: 100px; - } - h1 { - margin-top: 20px; - color: gray; - } -} - table#listing_products.bulk { clear: both; diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index a73c0cea07..08a05595c5 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -92,10 +92,11 @@ %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 }} - %div.loading{ :class => "sixteen columns alpha", 'ng-show' => 'loading' } - %h4 Loading Line Items... + %div.sixteen.columns.alpha#loading{ 'ng-if' => 'loading' } + %img.spinner{ src: "/assets/loading.gif" } + %h1 LOADING ORDERS %div{ :class => "sixteen columns alpha", 'ng-show' => '!loading && filteredLineItems.length == 0'} - %h4{ :style => 'color:red;' } No matching line items found. + %h1#no_results No orders found. %div{ 'ng-hide' => 'loading || filteredLineItems.length == 0' } %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } %thead diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index 522fcf9458..206a336a5e 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -54,7 +54,7 @@ %img.spinner{ src: "/assets/loading.gif" } %h1 LOADING PRODUCTS %div.sixteen.columns.alpha{ 'ng-show' => '!loading && filteredProducts.length == 0' } - %h1#no_products No products found. + %h1#no_results No products found. %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || filteredProducts.length == 0' } %table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" } %colgroup From d8c182332837c3b2a3adb186d1ec19a7a207d5f0 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 8 Aug 2014 14:01:43 +1000 Subject: [PATCH 111/205] Fix broken specs --- spec/features/admin/bulk_order_management_spec.rb | 2 +- spec/serializers/spree/variant_serializer_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 963cd04f8d..a8fe6cbda7 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -24,7 +24,7 @@ feature %q{ it "displays a message when number of line items is zero" do visit '/admin/orders/bulk_management' - page.should have_text "No matching line items found." + page.should have_text "No orders found." end diff --git a/spec/serializers/spree/variant_serializer_spec.rb b/spec/serializers/spree/variant_serializer_spec.rb index 891298541f..e0f26d3423 100644 --- a/spec/serializers/spree/variant_serializer_spec.rb +++ b/spec/serializers/spree/variant_serializer_spec.rb @@ -2,6 +2,6 @@ describe Spree::Api::VariantSerializer do let(:variant) { create(:variant) } it "serializes a variant" do serializer = Spree::Api::VariantSerializer.new variant - serializer.to_json.should match variant.name + serializer.to_json.should match variant.options_text end end \ No newline at end of file From 5fea15e8a93e1ad29d6610b5fd14b6fc4497093a Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 8 Aug 2014 14:02:15 +1000 Subject: [PATCH 112/205] Better error reporting for failed save on BPE --- .../javascripts/admin/bulk_product_update.js.coffee | 9 +++++++-- .../spree/admin/products_controller_decorator.rb | 6 +++++- .../unit/bulk_product_update_spec.js.coffee | 12 ++++++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 1b0ec47698..35ec6e694c 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -271,7 +271,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.updateVariantLists(data.products) $timeout -> $scope.displaySuccess() ).error (data, status) -> - $scope.displayFailure "Server returned with error status: " + status + if status == 400 && data.errors? && data.errors.length > 0 + errors = error + "\n" for error in data.errors + alert "Saving failed with the following error(s):\n" + errors + $scope.displayFailure "Save failed due to invalid data" + else + $scope.displayFailure "Server returned with error status: " + status $scope.packProduct = (product) -> @@ -336,7 +341,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.displayFailure = (failMessage) -> $scope.setMessage $scope.updateStatusMessage, "Saving failed. " + failMessage, color: "#DA5354" - , 10000 + , false $scope.displayDirtyProducts = -> diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 8b3b42feea..9f4fdbe46b 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -33,7 +33,11 @@ Spree::Admin::ProductsController.class_eval do if product_set.save redirect_to "/api/products/bulk_products?page=1;per_page=500;#{bulk_index_query}" else - render :nothing => true, :status => 418 + if product_set.errors.present? + render json: { errors: product_set.errors }, status: 400 + else + render :nothing => true, :status => 500 + end end end diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 270af2aaeb..b9081a1882 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -842,14 +842,22 @@ describe "AdminProductEditCtrl", -> expect(DirtyProducts.clear).toHaveBeenCalled() expect($scope.updateVariantLists).toHaveBeenCalled() - it "runs displayFailure() when post returns error", -> + it "runs displayFailure() when post returns an error", -> spyOn $scope, "displayFailure" $scope.products = "updated list of products" - $httpBackend.expectPOST("/admin/products/bulk_update").respond 404, "updated list of products" + $httpBackend.expectPOST("/admin/products/bulk_update").respond 500, "updated list of products" $scope.updateProducts "updated list of products" $httpBackend.flush() expect($scope.displayFailure).toHaveBeenCalled() + it "shows an alert with error information when post returns 400 with an errors array", -> + spyOn(window, "alert") + $scope.products = "updated list of products" + $httpBackend.expectPOST("/admin/products/bulk_update").respond 400, { "errors": ["an error"] } + $scope.updateProducts "updated list of products" + $httpBackend.flush() + expect(window.alert).toHaveBeenCalledWith("Saving failed with the following error(s):\nan error\n") + describe "fetching a product by id", -> it "returns the product when it is present", -> product = {id: 123} From 25d7adac837886ee428fb4b2c702785ed6615019 Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 3 Jul 2014 15:24:27 +1000 Subject: [PATCH 113/205] WIP: Replace configuration menu on payment methods edit page --- .../spree/admin/payment_methods_controller_decorator.rb | 7 +++++++ .../_form/add_distributors.html.haml.deface | 8 -------- .../edit/add_hubs_sidebar.html.haml.deface | 3 +++ .../edit/remove_configuration_sidebar.deface | 1 + .../payment_methods/new/add_hubs_sidebar.html.haml.deface | 3 +++ .../new/remove_configuration_sidebar.deface | 1 + 6 files changed, 15 insertions(+), 8 deletions(-) delete mode 100644 app/overrides/spree/admin/payment_methods/_form/add_distributors.html.haml.deface create mode 100644 app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface create mode 100644 app/overrides/spree/admin/payment_methods/edit/remove_configuration_sidebar.deface create mode 100644 app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface create mode 100644 app/overrides/spree/admin/payment_methods/new/remove_configuration_sidebar.deface diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index 11f707d9e6..90f85d2bc2 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -1,4 +1,6 @@ Spree::Admin::PaymentMethodsController.class_eval do + before_filter :load_hubs, only: [:new, :edit, :create, :update] + # Only show payment methods that user has access to and sort by distributor name # ! Redundant code copied from Spree::Admin::ResourceController with modifications marked def collection @@ -22,4 +24,9 @@ Spree::Admin::PaymentMethodsController.class_eval do collection end + + private + def load_hubs + @hubs = Enterprise.managed_by(spree_current_user).is_distributor.sort_by!{ |d| [(@payment_method.has_distributor? d) ? 0 : 1, d.name] } + end end diff --git a/app/overrides/spree/admin/payment_methods/_form/add_distributors.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/add_distributors.html.haml.deface deleted file mode 100644 index 0ed490a94a..0000000000 --- a/app/overrides/spree/admin/payment_methods/_form/add_distributors.html.haml.deface +++ /dev/null @@ -1,8 +0,0 @@ -/ insert_before '[data-hook="environment"]' - -= f.field_container :distributors do - = f.label :distributors - %br - - distributors = Enterprise.is_distributor.managed_by(spree_current_user) | f.object.distributors - = f.collection_select(:distributor_ids, distributors, :id, :name, {include_blank: false}, {class: "select2 fullwidth", multiple: true}) - = f.error_message_on :distributors diff --git a/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface new file mode 100644 index 0000000000..74fc957e71 --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" + += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/edit/remove_configuration_sidebar.deface b/app/overrides/spree/admin/payment_methods/edit/remove_configuration_sidebar.deface new file mode 100644 index 0000000000..cc3fbcdee1 --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/edit/remove_configuration_sidebar.deface @@ -0,0 +1 @@ +remove "code[erb-loud]:contains(\"render :partial => 'spree/admin/shared/configuration_menu'\")" \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface new file mode 100644 index 0000000000..74fc957e71 --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" + += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/new/remove_configuration_sidebar.deface b/app/overrides/spree/admin/payment_methods/new/remove_configuration_sidebar.deface new file mode 100644 index 0000000000..cc3fbcdee1 --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/new/remove_configuration_sidebar.deface @@ -0,0 +1 @@ +remove "code[erb-loud]:contains(\"render :partial => 'spree/admin/shared/configuration_menu'\")" \ No newline at end of file From 1cbdd9a5fa556d3a74eb115d988fb9baaf0635f6 Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 3 Jul 2014 17:08:32 +1000 Subject: [PATCH 114/205] WIP: rearrange payment methods page --- .../_form/remove_clearing_div.deface | 1 + .../replace_form_fields.html.haml.deface | 46 +++++++++++++++++++ .../edit/add_hubs_sidebar.html.haml.deface | 4 +- .../new/add_hubs_sidebar.html.haml.deface | 4 +- .../replace_form_fields.html.haml.deface | 4 +- .../edit/add_hubs_sidebar.html.haml.deface | 1 + .../new/add_hubs_sidebar.html.haml.deface | 1 + 7 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 app/overrides/spree/admin/payment_methods/_form/remove_clearing_div.deface create mode 100644 app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface diff --git a/app/overrides/spree/admin/payment_methods/_form/remove_clearing_div.deface b/app/overrides/spree/admin/payment_methods/_form/remove_clearing_div.deface new file mode 100644 index 0000000000..613ffe010b --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/_form/remove_clearing_div.deface @@ -0,0 +1 @@ +remove "div.clear" \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface new file mode 100644 index 0000000000..05737bcbf6 --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface @@ -0,0 +1,46 @@ +/ replace "div[data-hook='admin_payment_method_form_fields']" + +%div.alpha.eleven.columns + .row + .alpha.three.columns + = label_tag nil, t(:name) + .omega.eight.columns + = text_field :payment_method, :name, :class => 'fullwidth' + .row + .alpha.three.columns + = label_tag nil, t(:description) + .omega.eight.columns + = text_area :payment_method, :description, {:cols => 60, :rows => 6, :class => 'fullwidth'} + - if spree_current_user.admin? + .row + .alpha.three.columns + = label_tag nil, t(:environment) + .omega.eight.columns + = collection_select(:payment_method, :environment, Rails.configuration.database_configuration.keys.sort, :to_s, :titleize, {}, {:id => 'gtwy-env', :class => 'select2 fullwidth'}) + .row + .alpha.three.columns + = label_tag nil, t(:display) + .omega.eight.columns + = select(:payment_method, :display_on, Spree::PaymentMethod::DISPLAY.collect { |display| [t(display), display == :both ? nil : display.to_s] }, {}, {:class => 'select2 fullwidth'}) + .row + .alpha.three.columns + = label_tag nil, t(:active) + .two.columns + = radio_button :payment_method, :active, true +   + = label_tag nil, t(:say_yes) + .omega.six.columns + = radio_button :payment_method, :active, false +   + = label_tag nil, t(:say_no) + + %fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields + %legend{ align="center"} + = t(:gateway) + #preference-settings.field{"data-hook" => ""} + = f.label :type, t(:provider) + = collection_select(:payment_method, :type, @providers, :to_s, :name, {}, {:id => 'gtwy-type', :class => 'select2 fullwidth'}) + - unless @object.new_record? + = preference_fields(@object, f) + - if @object.respond_to?(:preferences) + #gateway-settings-warning.info.warning= t(:provider_settings_warning) \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface index 74fc957e71..3d28a1325e 100644 --- a/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface @@ -1,3 +1,5 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" -= render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file +.one.column   += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } +.clear \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface index 74fc957e71..3d28a1325e 100644 --- a/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface @@ -1,3 +1,5 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" -= render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file +.one.column   += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } +.clear \ No newline at end of file diff --git a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface index 5d0fce3841..190cff1a17 100644 --- a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface @@ -1,6 +1,6 @@ / replace "div[data-hook='admin_shipping_method_form_fields']" -.alpha.twelve.columns{"data-hook" => "admin_shipping_method_form_fields"} +.alpha.eleven.columns{"data-hook" => "admin_shipping_method_form_fields"} .row .alpha.three.columns = f.label :name, t(:name) @@ -41,7 +41,7 @@ = f.radio_button :require_ship_address, true   = f.label :delivery, t(:delivery) - .six.columns + .omega.six.columns = f.radio_button :require_ship_address, false   = f.label :pick_up, t(:pick_up) diff --git a/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface index 74fc957e71..df31849114 100644 --- a/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface @@ -1,3 +1,4 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" +.one.column   = render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file diff --git a/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface index 74fc957e71..df31849114 100644 --- a/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface @@ -1,3 +1,4 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" +.one.column   = render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file From 55e29832e15919eedecea1b78de5f0dd3b53d511 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 4 Jul 2014 11:43:10 +1000 Subject: [PATCH 115/205] Make name of payment_methods more human readable --- app/models/spree/payment_method_decorator.rb | 17 +++++++++++++++++ .../_form/replace_form_fields.html.haml.deface | 11 +++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index ddbc0251a9..c7cdf3a7e4 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -30,4 +30,21 @@ end # Ensure that all derived classes also allow distributor_ids Spree::Gateway.providers.each do |p| p.attr_accessible :distributor_ids + p.instance_eval do + def clean_name + case name + when "Spree::PaymentMethod::Check" + "Cash/EFT/etc. (payments for which automatic validation is not required)" + when "Spree::Gateway::Migs" + "MasterCard Internet Gateway Service (MIGS)" + when "Spree::BillingIntegration::PaypalExpressUk" + "PayPal Express (UK)" + when "Spree::BillingIntegration::PaypalExpress" + "PayPal Express" + else + i = name.rindex('::') + 2 + name[i..-1] + end + end + end end diff --git a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface index 05737bcbf6..0ae2b81a9e 100644 --- a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface @@ -33,13 +33,16 @@ = radio_button :payment_method, :active, false   = label_tag nil, t(:say_no) + .row + .alpha.three.columns + = f.label :type, t(:provider) + .omega.eight.columns + = collection_select(:payment_method, :type, @providers, :to_s, :clean_name, {}, {:id => 'gtwy-type', :class => 'select2 fullwidth'}) %fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields %legend{ align="center"} - = t(:gateway) - #preference-settings.field{"data-hook" => ""} - = f.label :type, t(:provider) - = collection_select(:payment_method, :type, @providers, :to_s, :name, {}, {:id => 'gtwy-type', :class => 'select2 fullwidth'}) + = t(:provider_settings) + #preference-settings - unless @object.new_record? = preference_fields(@object, f) - if @object.respond_to?(:preferences) From d6aae0050f89c77eaa2589e5314161c74435ae7b Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 4 Jul 2014 17:20:58 +1000 Subject: [PATCH 116/205] Angularise payment method provider UI --- .../providers_controller.js.coffee | 2 + .../directives/fetch_provider_prefs.js.coffee | 5 ++ .../payment_methods_controller_decorator.rb | 62 ++++++++++++------- app/models/spree/ability_decorator.rb | 2 +- .../replace_form_fields.html.haml.deface | 15 +---- .../_provider_settings.html.haml | 7 +++ .../payment_methods/_providers.html.haml | 10 +++ config/routes.rb | 4 ++ 8 files changed, 68 insertions(+), 39 deletions(-) create mode 100644 app/assets/javascripts/admin/controllers/providers_controller.js.coffee create mode 100644 app/assets/javascripts/admin/directives/fetch_provider_prefs.js.coffee create mode 100644 app/views/spree/admin/payment_methods/_provider_settings.html.haml create mode 100644 app/views/spree/admin/payment_methods/_providers.html.haml diff --git a/app/assets/javascripts/admin/controllers/providers_controller.js.coffee b/app/assets/javascripts/admin/controllers/providers_controller.js.coffee new file mode 100644 index 0000000000..b78ca47f4c --- /dev/null +++ b/app/assets/javascripts/admin/controllers/providers_controller.js.coffee @@ -0,0 +1,2 @@ +angular.module("ofn.admin").controller "ProvidersCtrl", ($scope, paymentMethod) -> + $scope.include_html = "/admin/payment_methods/#{paymentMethod.id}/show_provider_preferences?provider_type=#{paymentMethod.type}" \ No newline at end of file diff --git a/app/assets/javascripts/admin/directives/fetch_provider_prefs.js.coffee b/app/assets/javascripts/admin/directives/fetch_provider_prefs.js.coffee new file mode 100644 index 0000000000..dad8e4b8a1 --- /dev/null +++ b/app/assets/javascripts/admin/directives/fetch_provider_prefs.js.coffee @@ -0,0 +1,5 @@ +angular.module("ofn.admin").directive "ofnFetchProviderPrefs", ($http) -> + link: (scope, element, attrs) -> + element.on "change blur", -> + scope.$apply -> + scope.include_html = "/admin/payment_methods/#{attrs.ofnFetchProviderPrefs}/show_provider_preferences?provider_type=#{element.val()}" \ No newline at end of file diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index 90f85d2bc2..fdb2c0953a 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -1,32 +1,46 @@ -Spree::Admin::PaymentMethodsController.class_eval do - before_filter :load_hubs, only: [:new, :edit, :create, :update] +module Spree + module Admin + PaymentMethodsController.class_eval do + before_filter :load_hubs, only: [:new, :edit, :create, :update] - # Only show payment methods that user has access to and sort by distributor name - # ! Redundant code copied from Spree::Admin::ResourceController with modifications marked - def collection - return parent.send(controller_name) if parent_data.present? - collection = if model_class.respond_to?(:accessible_by) && - !current_ability.has_block?(params[:action], model_class) + # Only show payment methods that user has access to and sort by distributor name + # ! Redundant code copied from Spree::Admin::ResourceController with modifications marked + def collection + return parent.send(controller_name) if parent_data.present? + collection = if model_class.respond_to?(:accessible_by) && + !current_ability.has_block?(params[:action], model_class) - model_class.accessible_by(current_ability, action) + model_class.accessible_by(current_ability, action) - else - model_class.scoped - end + else + model_class.scoped + end - collection = collection.managed_by(spree_current_user).by_name # This line added + collection = collection.managed_by(spree_current_user).by_name # This line added - # This block added - if params.key? :enterprise_id - distributor = Enterprise.find params[:enterprise_id] - collection = collection.for_distributor(distributor) + # This block added + if params.key? :enterprise_id + distributor = Enterprise.find params[:enterprise_id] + collection = collection.for_distributor(distributor) + end + + collection + end + + def show_provider_preferences + @payment_method = PaymentMethod.find(params[:id]) + payment_method_type = params[:provider_type] + if @payment_method['type'].to_s != payment_method_type + @payment_method.update_column(:type, payment_method_type) + @payment_method = PaymentMethod.find(params[:id]) + end + render partial: 'provider_settings' + end + + private + def load_hubs + @hubs = Enterprise.managed_by(spree_current_user).is_distributor.sort_by!{ |d| [(@payment_method.has_distributor? d) ? 0 : 1, d.name] } + end end - - collection - end - - private - def load_hubs - @hubs = Enterprise.managed_by(spree_current_user).is_distributor.sort_by!{ |d| [(@payment_method.has_distributor? d) ? 0 : 1, d.name] } end end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index e6d93f25aa..a04d966ee7 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -44,7 +44,7 @@ class AbilityDecorator # Enterprise User can only access payment methods for their distributors can [:index, :create], Spree::PaymentMethod - can [:admin, :read, :update, :fire, :resend, :destroy], Spree::PaymentMethod do |payment_method| + can [:admin, :read, :update, :fire, :resend, :destroy, :show_provider_preferences], Spree::PaymentMethod do |payment_method| (user.enterprises & payment_method.distributors).any? end diff --git a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface index 0ae2b81a9e..3b588a98c1 100644 --- a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface @@ -33,17 +33,4 @@ = radio_button :payment_method, :active, false   = label_tag nil, t(:say_no) - .row - .alpha.three.columns - = f.label :type, t(:provider) - .omega.eight.columns - = collection_select(:payment_method, :type, @providers, :to_s, :clean_name, {}, {:id => 'gtwy-type', :class => 'select2 fullwidth'}) - - %fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields - %legend{ align="center"} - = t(:provider_settings) - #preference-settings - - unless @object.new_record? - = preference_fields(@object, f) - - if @object.respond_to?(:preferences) - #gateway-settings-warning.info.warning= t(:provider_settings_warning) \ No newline at end of file + = render 'providers' \ No newline at end of file diff --git a/app/views/spree/admin/payment_methods/_provider_settings.html.haml b/app/views/spree/admin/payment_methods/_provider_settings.html.haml new file mode 100644 index 0000000000..e227180fc3 --- /dev/null +++ b/app/views/spree/admin/payment_methods/_provider_settings.html.haml @@ -0,0 +1,7 @@ +- if @payment_method.preferences.present? + %fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields + %legend{ align: "center"} + = t(:provider_settings) + .preference-settings + = fields_for :payment_method, @payment_method do |payment_method_form| + = preference_fields(@payment_method, payment_method_form) \ No newline at end of file diff --git a/app/views/spree/admin/payment_methods/_providers.html.haml b/app/views/spree/admin/payment_methods/_providers.html.haml new file mode 100644 index 0000000000..825e277a95 --- /dev/null +++ b/app/views/spree/admin/payment_methods/_providers.html.haml @@ -0,0 +1,10 @@ +:javascript + angular.module('ofn.admin').value('paymentMethod', #{ { id: @payment_method.id, type: @payment_method.type }.to_json }) +#provider-settings{ ng: { app: "ofn.admin", controller: "ProvidersCtrl" } } + .row + .alpha.three.columns + = label :payment_method, :type, t(:provider) + .omega.eight.columns + = collection_select(:payment_method, :type, @providers, :to_s, :clean_name, {}, { class: 'select2 fullwidth', 'ofn-fetch-provider-prefs' => "#{@object.id}"}) + + %div{"ng-include" => "include_html" } \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index af70141639..53f7b2a92e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -135,6 +135,10 @@ Spree::Core::Engine.routes.prepend do post :bulk_update, :on => :collection, :as => :bulk_update end + + resources :payment_methods do + get :show_provider_preferences, on: :member + end end resources :orders do From f021d260b13e945f5828378c42703c0b1cc24a73 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 9 Jul 2014 10:47:16 +1000 Subject: [PATCH 117/205] Require payment_methods to be associated with at least one hub --- .../spree/admin/payment_methods_controller_decorator.rb | 3 ++- app/models/spree/payment_method_decorator.rb | 2 ++ spec/features/admin/payment_method_spec.rb | 8 ++++++-- spec/models/spree/payment_method_spec.rb | 6 ++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index fdb2c0953a..da98806027 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -1,7 +1,8 @@ module Spree module Admin PaymentMethodsController.class_eval do - before_filter :load_hubs, only: [:new, :edit, :create, :update] + before_filter :load_hubs, only: [:new, :edit, :update] + create.before :load_hubs # Only show payment methods that user has access to and sort by distributor name # ! Redundant code copied from Spree::Admin::ResourceController with modifications marked diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index c7cdf3a7e4..37661463c9 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -4,6 +4,8 @@ Spree::PaymentMethod.class_eval do attr_accessible :distributor_ids + validates :distributors, presence: { message: "^At least one hub must be selected" } + # -- Scopes scope :managed_by, lambda { |user| if user.has_spree_role?('admin') diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index 660a187b3d..a5e8cbccec 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -46,14 +46,18 @@ feature %q{ login_to_admin_as enterprise_user end - it "creates payment methods" do + it "I can get to the new enterprise page" do click_link 'Enterprises' within(".enterprise-#{distributor1.id}") { click_link 'Payment Methods' } click_link 'New Payment Method' + current_path.should == spree.new_admin_payment_method_path + end + it "creates payment methods" do + visit spree.new_admin_payment_method_path fill_in 'payment_method_name', :with => 'Cheque payment method' - select distributor1.name, :from => 'payment_method_distributor_ids' + check "payment_method_distributor_ids_#{distributor1.id}" click_button 'Create' flash_message.should == 'Payment Method has been successfully created!' diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index ef70afd426..bed15fdbef 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -9,5 +9,11 @@ module Spree PaymentMethod.by_name.should == [pm2, pm3, pm1] end + + it "raises errors when required fields are missing" do + pm = PaymentMethod.new() + pm.save + pm.errors.to_a.should == ["Name can't be blank", "At least one hub must be selected"] + end end end From 16215289d0a2e5c0ef4572a1a838340757bddccd Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 9 Jul 2014 11:41:16 +1000 Subject: [PATCH 118/205] Spec for payment method clean_name translation --- spec/models/spree/payment_method_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index bed15fdbef..394ff490a2 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -15,5 +15,15 @@ module Spree pm.save pm.errors.to_a.should == ["Name can't be blank", "At least one hub must be selected"] end + + it "generates a clean name for known Payment Method types" do + Spree::PaymentMethod::Check.clean_name.should == "Cash/EFT/etc. (payments for which automatic validation is not required)" + Spree::Gateway::Migs.clean_name.should == "MasterCard Internet Gateway Service (MIGS)" + Spree::BillingIntegration::PaypalExpressUk.clean_name.should == "PayPal Express (UK)" + Spree::BillingIntegration::PaypalExpress.clean_name.should == "PayPal Express" + + # Testing else condition + Spree::Gateway::BogusSimple.clean_name.should == "BogusSimple" + end end end From f4302673049a0f88cdeb15b6bf5486af66d40b2c Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 9 Jul 2014 15:02:43 +1000 Subject: [PATCH 119/205] Specs for show_provider_preferences --- .../admin/payment_methods_controller_spec.rb | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 spec/controllers/spree/admin/payment_methods_controller_spec.rb diff --git a/spec/controllers/spree/admin/payment_methods_controller_spec.rb b/spec/controllers/spree/admin/payment_methods_controller_spec.rb new file mode 100644 index 0000000000..492afd8078 --- /dev/null +++ b/spec/controllers/spree/admin/payment_methods_controller_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Spree::Admin::PaymentMethodsController do + context "Requesting provider preference fields" do + let(:user) do + user = create(:user) + user.spree_roles << Spree::Role.find_or_create_by_name!('admin') + user + end + + let(:payment_method) { create(:payment_method) } + + before do + controller.stub spree_current_user: user + end + + context "without an altered provider type" do + it "renders provider settings with same payment method" do + spree_get :show_provider_preferences, { + id: payment_method.id, + provider_type: "Spree::PaymentMethod::Check" + } + Spree::PaymentMethod.find(payment_method.id).should == payment_method + response.should render_template partial: '_provider_settings' + end + end + + context "with an altered provider type" do + it "renders provider settings with a different payment method" do + spree_get :show_provider_preferences, { + id: payment_method.id, + provider_type: "Spree::Gateway::Bogus" + } + Spree::PaymentMethod.find(payment_method.id).should_not == payment_method + response.should render_template partial: '_provider_settings' + end + end + end +end \ No newline at end of file From f19af5255634663bb5b6e0b5fd4a312f67e6ca28 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 11 Jul 2014 16:19:30 +1000 Subject: [PATCH 120/205] WIP: angularised provider settings for payment method works on create (for admin only at this stage) --- .../providers_controller.js.coffee | 7 ++- .../directives/fetch_provider_prefs.js.coffee | 5 --- .../directives/provider_prefs_for.js.coffee | 7 +++ .../payment_methods_controller_decorator.rb | 14 +++--- app/models/spree/ability_decorator.rb | 4 +- .../payment_methods/_providers.html.haml | 2 +- config/routes.rb | 5 +-- .../admin/payment_methods_controller_spec.rb | 43 ++++++++++++------- 8 files changed, 54 insertions(+), 33 deletions(-) delete mode 100644 app/assets/javascripts/admin/directives/fetch_provider_prefs.js.coffee create mode 100644 app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee diff --git a/app/assets/javascripts/admin/controllers/providers_controller.js.coffee b/app/assets/javascripts/admin/controllers/providers_controller.js.coffee index b78ca47f4c..3b3378d013 100644 --- a/app/assets/javascripts/admin/controllers/providers_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/providers_controller.js.coffee @@ -1,2 +1,7 @@ angular.module("ofn.admin").controller "ProvidersCtrl", ($scope, paymentMethod) -> - $scope.include_html = "/admin/payment_methods/#{paymentMethod.id}/show_provider_preferences?provider_type=#{paymentMethod.type}" \ No newline at end of file + if paymentMethod.type + $scope.include_html = "/admin/payment_methods/show_provider_preferences?" + + "provider_type=#{paymentMethod.type};" + + "pm_id=#{paymentMethod.id};" + else + $scope.include_html = "" \ No newline at end of file diff --git a/app/assets/javascripts/admin/directives/fetch_provider_prefs.js.coffee b/app/assets/javascripts/admin/directives/fetch_provider_prefs.js.coffee deleted file mode 100644 index dad8e4b8a1..0000000000 --- a/app/assets/javascripts/admin/directives/fetch_provider_prefs.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -angular.module("ofn.admin").directive "ofnFetchProviderPrefs", ($http) -> - link: (scope, element, attrs) -> - element.on "change blur", -> - scope.$apply -> - scope.include_html = "/admin/payment_methods/#{attrs.ofnFetchProviderPrefs}/show_provider_preferences?provider_type=#{element.val()}" \ No newline at end of file diff --git a/app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee b/app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee new file mode 100644 index 0000000000..467bad4e5f --- /dev/null +++ b/app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee @@ -0,0 +1,7 @@ +angular.module("ofn.admin").directive "providerPrefsFor", ($http) -> + link: (scope, element, attrs) -> + element.on "change blur load", -> + scope.$apply -> + scope.include_html = "/admin/payment_methods/show_provider_preferences?" + + "provider_type=#{element.val()};" + + "pm_id=#{attrs.providerPrefsFor};" \ No newline at end of file diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index da98806027..648d249d71 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -29,11 +29,15 @@ module Spree end def show_provider_preferences - @payment_method = PaymentMethod.find(params[:id]) - payment_method_type = params[:provider_type] - if @payment_method['type'].to_s != payment_method_type - @payment_method.update_column(:type, payment_method_type) - @payment_method = PaymentMethod.find(params[:id]) + if params[:pm_id].present? + @payment_method = PaymentMethod.find(params[:pm_id]) + payment_method_type = params[:provider_type] + if @payment_method['type'].to_s != payment_method_type + @payment_method.update_column(:type, payment_method_type) + @payment_method = PaymentMethod.find(params[:pm_id]) + end + else + @payment_method = params[:provider_type].constantize.new() end render partial: 'provider_settings' end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index a04d966ee7..5d4644fafc 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -43,8 +43,8 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization # Enterprise User can only access payment methods for their distributors - can [:index, :create], Spree::PaymentMethod - can [:admin, :read, :update, :fire, :resend, :destroy, :show_provider_preferences], Spree::PaymentMethod do |payment_method| + can [:index, :create, :show_provider_preferences], Spree::PaymentMethod + can [:admin, :read, :update, :fire, :resend, :destroy], Spree::PaymentMethod do |payment_method| (user.enterprises & payment_method.distributors).any? end diff --git a/app/views/spree/admin/payment_methods/_providers.html.haml b/app/views/spree/admin/payment_methods/_providers.html.haml index 825e277a95..77b11fb473 100644 --- a/app/views/spree/admin/payment_methods/_providers.html.haml +++ b/app/views/spree/admin/payment_methods/_providers.html.haml @@ -5,6 +5,6 @@ .alpha.three.columns = label :payment_method, :type, t(:provider) .omega.eight.columns - = collection_select(:payment_method, :type, @providers, :to_s, :clean_name, {}, { class: 'select2 fullwidth', 'ofn-fetch-provider-prefs' => "#{@object.id}"}) + = collection_select(:payment_method, :type, @providers, :to_s, :clean_name, (!@object.persisted? ? { :selected => "Spree::PaymentMethod::Check"} : {}), { class: 'select2 fullwidth', 'provider-prefs-for' => "#{@object.id}"}) %div{"ng-include" => "include_html" } \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 53f7b2a92e..b9c0a2da55 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -107,6 +107,7 @@ Spree::Core::Engine.routes.prepend do 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', :to => 'admin/overview#index', :as => :admin + match '/admin/payment_methods/show_provider_preferences' => 'admin/payment_methods#show_provider_preferences', :via => :get namespace :api, :defaults => { :format => 'json' } do @@ -135,10 +136,6 @@ Spree::Core::Engine.routes.prepend do post :bulk_update, :on => :collection, :as => :bulk_update end - - resources :payment_methods do - get :show_provider_preferences, on: :member - end end resources :orders do diff --git a/spec/controllers/spree/admin/payment_methods_controller_spec.rb b/spec/controllers/spree/admin/payment_methods_controller_spec.rb index 492afd8078..ad85f590a5 100644 --- a/spec/controllers/spree/admin/payment_methods_controller_spec.rb +++ b/spec/controllers/spree/admin/payment_methods_controller_spec.rb @@ -8,31 +8,44 @@ describe Spree::Admin::PaymentMethodsController do user end - let(:payment_method) { create(:payment_method) } - before do controller.stub spree_current_user: user end - context "without an altered provider type" do - it "renders provider settings with same payment method" do - spree_get :show_provider_preferences, { - id: payment_method.id, - provider_type: "Spree::PaymentMethod::Check" - } - Spree::PaymentMethod.find(payment_method.id).should == payment_method - response.should render_template partial: '_provider_settings' + context "on an existing payment method" do + let(:payment_method) { create(:payment_method) } + + context "without an altered provider type" do + it "renders provider settings with same payment method" do + spree_get :show_provider_preferences, { + pm_id: payment_method.id, + provider_type: "Spree::PaymentMethod::Check" + } + expect(assigns(:payment_method)).to eq payment_method + expect(response).to render_template partial: '_provider_settings' + end + end + + context "with an altered provider type" do + it "renders provider settings with a different payment method" do + spree_get :show_provider_preferences, { + pm_id: payment_method.id, + provider_type: "Spree::Gateway::Bogus" + } + expect(assigns(:payment_method)).not_to eq payment_method + expect(response).to render_template partial: '_provider_settings' + end end end - context "with an altered provider type" do - it "renders provider settings with a different payment method" do + context "on a new payment method" do + it "renders provider settings with a new payment method of type" do spree_get :show_provider_preferences, { - id: payment_method.id, + pm_id: "", provider_type: "Spree::Gateway::Bogus" } - Spree::PaymentMethod.find(payment_method.id).should_not == payment_method - response.should render_template partial: '_provider_settings' + expect(assigns(:payment_method)).to be_a_new Spree::Gateway::Bogus + expect(response).to render_template partial: '_provider_settings' end end end From d80166e80d108dbb093d962976cc6e81d64b9268 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 23 Jul 2014 12:06:30 +1000 Subject: [PATCH 121/205] Restrict access to show_provider_preferences action on payment methods controller --- .../payment_methods_controller_decorator.rb | 2 + app/models/spree/ability_decorator.rb | 4 +- .../admin/payment_methods_controller_spec.rb | 58 +++++++++++++------ 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index 648d249d71..2e5dfa19fd 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -1,6 +1,7 @@ module Spree module Admin PaymentMethodsController.class_eval do + skip_before_filter :load_resource, only: [:show_provider_preferences] before_filter :load_hubs, only: [:new, :edit, :update] create.before :load_hubs @@ -31,6 +32,7 @@ module Spree def show_provider_preferences if params[:pm_id].present? @payment_method = PaymentMethod.find(params[:pm_id]) + authorize! :show_provider_preferences, @payment_method payment_method_type = params[:provider_type] if @payment_method['type'].to_s != payment_method_type @payment_method.update_column(:type, payment_method_type) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 5d4644fafc..a04d966ee7 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -43,8 +43,8 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization # Enterprise User can only access payment methods for their distributors - can [:index, :create, :show_provider_preferences], Spree::PaymentMethod - can [:admin, :read, :update, :fire, :resend, :destroy], Spree::PaymentMethod do |payment_method| + can [:index, :create], Spree::PaymentMethod + can [:admin, :read, :update, :fire, :resend, :destroy, :show_provider_preferences], Spree::PaymentMethod do |payment_method| (user.enterprises & payment_method.distributors).any? end diff --git a/spec/controllers/spree/admin/payment_methods_controller_spec.rb b/spec/controllers/spree/admin/payment_methods_controller_spec.rb index ad85f590a5..f3266f69c2 100644 --- a/spec/controllers/spree/admin/payment_methods_controller_spec.rb +++ b/spec/controllers/spree/admin/payment_methods_controller_spec.rb @@ -2,10 +2,13 @@ require 'spec_helper' describe Spree::Admin::PaymentMethodsController do context "Requesting provider preference fields" do + let(:enterprise) { create(:distributor_enterprise) } let(:user) do - user = create(:user) - user.spree_roles << Spree::Role.find_or_create_by_name!('admin') - user + new_user = create(:user, email: 'enterprise@hub.com', password: 'blahblah', :password_confirmation => 'blahblah', ) + new_user.spree_roles = [] # for some reason unbeknown to me, this new user gets admin permissions by default. + new_user.enterprise_roles.build(enterprise: enterprise).save + new_user.save + new_user end before do @@ -15,25 +18,46 @@ describe Spree::Admin::PaymentMethodsController do context "on an existing payment method" do let(:payment_method) { create(:payment_method) } - context "without an altered provider type" do - it "renders provider settings with same payment method" do + context "where I have permission" do + before do + payment_method.distributors << user.enterprises.is_distributor.first + end + + context "without an altered provider type" do + it "renders provider settings with same payment method" do + spree_get :show_provider_preferences, { + pm_id: payment_method.id, + provider_type: "Spree::PaymentMethod::Check" + } + expect(assigns(:payment_method)).to eq payment_method + expect(response).to render_template partial: '_provider_settings' + end + end + + context "with an altered provider type" do + it "renders provider settings with a different payment method" do + spree_get :show_provider_preferences, { + pm_id: payment_method.id, + provider_type: "Spree::Gateway::Bogus" + } + expect(assigns(:payment_method)).not_to eq payment_method + expect(response).to render_template partial: '_provider_settings' + end + end + end + + context "where I do not have permission" do + before do + payment_method.distributors = [] + end + + it "renders unauthorised" do spree_get :show_provider_preferences, { pm_id: payment_method.id, provider_type: "Spree::PaymentMethod::Check" } expect(assigns(:payment_method)).to eq payment_method - expect(response).to render_template partial: '_provider_settings' - end - end - - context "with an altered provider type" do - it "renders provider settings with a different payment method" do - spree_get :show_provider_preferences, { - pm_id: payment_method.id, - provider_type: "Spree::Gateway::Bogus" - } - expect(assigns(:payment_method)).not_to eq payment_method - expect(response).to render_template partial: '_provider_settings' + expect(flash[:error]).to eq "Authorization Failure" end end end From 9e54162a6252e84363da69905b9f19f0cc3108d9 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 23 Jul 2014 15:16:37 +1000 Subject: [PATCH 122/205] Annoying migration changes --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 7b31f581f1..84b51e8b93 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -553,9 +553,9 @@ ActiveRecord::Schema.define(:version => 20140723023713) 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 From 6b73eb435c5d4dbd351c2f0e2798dda17aa28866 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 23 Jul 2014 15:17:03 +1000 Subject: [PATCH 123/205] Add js specs for providers controller --- .../providers_controller_decorator.js.coffee | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee diff --git a/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee b/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee new file mode 100644 index 0000000000..2fcd776035 --- /dev/null +++ b/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee @@ -0,0 +1,30 @@ +describe "ProvidersCtrl", -> + ctrl = null + scope = null + paymentMethod = null + + describe "initialising using a payment method without a type", -> + beforeEach -> + module 'ofn.admin' + scope = {} + paymentMethod = + type: null + + inject ($controller)-> + ctrl = $controller 'ProvidersCtrl', {$scope: scope, paymentMethod: paymentMethod } + + it "sets the invlude_html porperty on scope to blank", -> + expect(scope.include_html).toBe "" + + describe "initialising using a payment method with a type", -> + beforeEach -> + module 'ofn.admin' + scope = {} + paymentMethod = + type: "NOT NULL" + + inject ($controller)-> + ctrl = $controller 'ProvidersCtrl', {$scope: scope, paymentMethod: paymentMethod } + + it "sets the include_html porperty on scope to some address", -> + expect(scope.include_html).toBe "/admin/payment_methods/show_provider_preferences?provider_type=NOT NULL;pm_id=#{paymentMethod.id};" \ No newline at end of file From 40d290951ce11ae46dc2045c3f40b5d445e87ab7 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 23 Jul 2014 16:06:02 +1000 Subject: [PATCH 124/205] Fix failing specs related to requiring distributor for payment method --- spec/features/admin/enterprises_spec.rb | 2 +- spec/features/admin/payment_method_spec.rb | 2 +- spec/models/spree/order_spec.rb | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 4d277972e5..6ec19098fe 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -113,7 +113,7 @@ feature %q{ e2 = create(:enterprise) eg1 = create(:enterprise_group, name: 'eg1') eg2 = create(:enterprise_group, name: 'eg2') - payment_method = create(:payment_method, distributors: []) + payment_method = create(:payment_method, distributors: [e2]) shipping_method = create(:shipping_method, distributors: [e2]) enterprise_fee = create(:enterprise_fee, enterprise: @enterprise ) diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index a5e8cbccec..9f9a0e43a8 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -21,7 +21,7 @@ feature %q{ fill_in 'payment_method_name', :with => 'Cheque payment method' - select @distributors[0].name, :from => 'payment_method_distributor_ids', visible: false + check "payment_method_distributor_ids_#{@distributors[0].id}" click_button 'Create' flash_message.should == 'Payment Method has been successfully created!' diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 085ee615c6..63c315a18a 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -22,9 +22,11 @@ describe Spree::Order do end describe "Payment methods" do - let(:order) { build(:order, distributor: create(:distributor_enterprise)) } - let(:pm1) { create(:payment_method, distributors: [order.distributor])} - let(:pm2) { create(:payment_method, distributors: [])} + let(:order_distributor) { create(:distributor_enterprise) } + let(:some_other_distributor) { create(:distributor_enterprise) } + let(:order) { build(:order, distributor: order_distributor) } + let(:pm1) { create(:payment_method, distributors: [order_distributor])} + let(:pm2) { create(:payment_method, distributors: [some_other_distributor])} it "finds the correct payment methods" do Spree::PaymentMethod.stub(:available).and_return [pm1, pm2] From 115d8e0d6e50af4a947b62ad31e91dcdd1118a2a Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 8 Aug 2014 17:37:42 +1000 Subject: [PATCH 125/205] Smoosh distributions up into cart item subtotal in order confirmation email --- app/helpers/checkout_helper.rb | 2 +- app/views/spree/order_mailer/confirm_email.text.erb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index 171a672cda..6441cd619e 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -22,7 +22,7 @@ module CheckoutHelper end def checkout_cart_total_with_adjustments(order) - current_order.display_item_total.money.to_f + checkout_adjustments_total(current_order).money.to_f + order.display_item_total.money.to_f + checkout_adjustments_total(order).money.to_f end diff --git a/app/views/spree/order_mailer/confirm_email.text.erb b/app/views/spree/order_mailer/confirm_email.text.erb index a72ada83e2..932eaebe4c 100644 --- a/app/views/spree/order_mailer/confirm_email.text.erb +++ b/app/views/spree/order_mailer/confirm_email.text.erb @@ -10,8 +10,8 @@ Order for: <%= @order.bill_address.full_name %> <%= item.variant.sku %> <%= raw(item.variant.product.supplier.name) %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (QTY: <%=item.quantity%>) @ <%= item.single_money %> = <%= item.display_amount %> <% end %> ============================================================ -Subtotal: <%= @order.display_item_total %> -<% checkout_adjustments_for_summary(@order).each do |adjustment| %> +Subtotal: <%= number_to_currency checkout_cart_total_with_adjustments(@order) %> +<% checkout_adjustments_for_summary(@order, exclude: [:distribution]).each do |adjustment| %> <%= raw(adjustment.label) %> <%= adjustment.display_amount %> <% end %> Order Total: <%= @order.display_total %> From 51f912033b9d2eb7b667cea855bc71c123730883 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 8 Aug 2014 22:39:26 +1000 Subject: [PATCH 126/205] Remove test for PayPalExpress UK which does not exist anymore --- spec/models/spree/payment_method_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index 394ff490a2..cfd78cfa70 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -19,7 +19,6 @@ module Spree it "generates a clean name for known Payment Method types" do Spree::PaymentMethod::Check.clean_name.should == "Cash/EFT/etc. (payments for which automatic validation is not required)" Spree::Gateway::Migs.clean_name.should == "MasterCard Internet Gateway Service (MIGS)" - Spree::BillingIntegration::PaypalExpressUk.clean_name.should == "PayPal Express (UK)" Spree::BillingIntegration::PaypalExpress.clean_name.should == "PayPal Express" # Testing else condition From a8b823668f50c204114e3da9c380b23b08bf136a Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 8 Aug 2014 22:40:24 +1000 Subject: [PATCH 127/205] Fix checkout specs failing due to payment method not having hub --- spec/features/consumer/shopping/checkout_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index e3e2c9c4ad..846fc57a9b 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -72,8 +72,7 @@ feature "As a consumer I want to check out my cart", js: true do let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") } let!(:pm2) { create(:payment_method, distributors: [distributor]) } let!(:pm3) do - Spree::Gateway::PayPalExpress.create!(name: "Paypal", environment: 'test').tap do |pm| - pm.distributors << distributor + Spree::Gateway::PayPalExpress.create!(name: "Paypal", environment: 'test', distributor_ids: [distributor.id]).tap do |pm| pm.preferred_login = 'devnull-facilitator_api1.rohanmitchell.com' pm.preferred_password = '1406163716' pm.preferred_signature = 'AFcWxV21C7fd0v3bYYYRCpSSRl31AaTntNJ-AjvUJkWf4dgJIvcLsf1V' From 044a4c68164d4a62b2bd118597078edbafea849c Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 8 Aug 2014 23:19:13 +1000 Subject: [PATCH 128/205] Finish changing class names for PayPal gateways --- app/models/spree/payment_method_decorator.rb | 4 +--- spec/models/spree/payment_method_spec.rb | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index 37661463c9..01f6e2f0e8 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -39,9 +39,7 @@ Spree::Gateway.providers.each do |p| "Cash/EFT/etc. (payments for which automatic validation is not required)" when "Spree::Gateway::Migs" "MasterCard Internet Gateway Service (MIGS)" - when "Spree::BillingIntegration::PaypalExpressUk" - "PayPal Express (UK)" - when "Spree::BillingIntegration::PaypalExpress" + when "Spree::Gateway::PayPalExpress" "PayPal Express" else i = name.rindex('::') + 2 diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index cfd78cfa70..692db7c7a4 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -19,7 +19,7 @@ module Spree it "generates a clean name for known Payment Method types" do Spree::PaymentMethod::Check.clean_name.should == "Cash/EFT/etc. (payments for which automatic validation is not required)" Spree::Gateway::Migs.clean_name.should == "MasterCard Internet Gateway Service (MIGS)" - Spree::BillingIntegration::PaypalExpress.clean_name.should == "PayPal Express" + Spree::Gateway::PayPalExpress.clean_name.should == "PayPal Express" # Testing else condition Spree::Gateway::BogusSimple.clean_name.should == "BogusSimple" From c20369919c11116b01a90db110754a93edcd2dda Mon Sep 17 00:00:00 2001 From: Rob H Date: Sat, 9 Aug 2014 11:23:05 +1000 Subject: [PATCH 129/205] Can change hub after changing pm type --- .../edit/add_hubs_sidebar.html.haml.deface | 2 +- .../new/add_hubs_sidebar.html.haml.deface | 2 +- .../edit/add_hubs_sidebar.html.haml.deface | 2 +- .../new/add_hubs_sidebar.html.haml.deface | 2 +- .../admin/shared/_hubs_sidebar.html.haml | 4 ++-- spec/features/admin/payment_method_spec.rb | 22 +++++++++++++++++++ 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface index 3d28a1325e..26ee2888bd 100644 --- a/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface @@ -1,5 +1,5 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" .one.column   -= render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { f: f, klass: :payment_method } .clear \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface index 3d28a1325e..26ee2888bd 100644 --- a/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface @@ -1,5 +1,5 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" .one.column   -= render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { f: f, klass: :payment_method } .clear \ No newline at end of file diff --git a/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface index df31849114..057013c5a6 100644 --- a/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface @@ -1,4 +1,4 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" .one.column   -= render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { f: f, klass: :shipping_method } \ No newline at end of file diff --git a/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface index df31849114..057013c5a6 100644 --- a/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface @@ -1,4 +1,4 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" .one.column   -= render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { f: f, klass: :shipping_method } \ No newline at end of file diff --git a/app/views/spree/admin/shared/_hubs_sidebar.html.haml b/app/views/spree/admin/shared/_hubs_sidebar.html.haml index 445a45ea6c..cdd367065a 100644 --- a/app/views/spree/admin/shared/_hubs_sidebar.html.haml +++ b/app/views/spree/admin/shared/_hubs_sidebar.html.haml @@ -5,13 +5,13 @@ %span.four.columns.alpha.centered Hubs .four.columns.alpha.list{ class: "#{hubs_color}" } - if @hubs.count > 0 - = f.hidden_field :distributor_ids, :multiple => true, value: nil + = hidden_field klass, :distributor_ids, :multiple => true, value: nil - @hubs.each do |hub| %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}" } %a.three.columns.alpha{ href: "#{main_app.edit_admin_enterprise_path(hub)}" } = hub.name %span.one.column.omega - = f.check_box :distributor_ids, { multiple: true }, hub.id, nil + = check_box klass, :distributor_ids, { multiple: true }, hub.id, nil - else .four.columns.alpha.list-item %span.three.columns.alpha None Available diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index 9f9a0e43a8..36fe344930 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -29,6 +29,28 @@ feature %q{ payment_method = Spree::PaymentMethod.find_by_name('Cheque payment method') payment_method.distributors.should == [@distributors[0]] end + + scenario "updating a payment method" do + pm = create(:payment_method, distributors: [@distributors[0]]) + login_to_admin_section + + visit spree.edit_admin_payment_method_path pm + + fill_in 'payment_method_name', :with => 'New PM Name' + + uncheck "payment_method_distributor_ids_#{@distributors[0].id}" + check "payment_method_distributor_ids_#{@distributors[1].id}" + check "payment_method_distributor_ids_#{@distributors[2].id}" + select2_select "PayPal Express", from: "payment_method_type" + click_button 'Update' + + expect(flash_message).to eq 'Payment Method has been successfully updated!' + + payment_method = Spree::PaymentMethod.find_by_name('New PM Name') + expect(payment_method.distributors).to include @distributors[1], @distributors[2] + expect(payment_method.distributors).not_to include @distributors[0] + expect(payment_method.type).to eq "Spree::Gateway::PayPalExpress" + end end context "as an enterprise user" do From a7b3bbee744bebed0fcb31005a41c165d30e8a48 Mon Sep 17 00:00:00 2001 From: Rob H Date: Sat, 9 Aug 2014 12:52:00 +1000 Subject: [PATCH 130/205] Overriding payment method load_data to hide Bogus Gateways --- .../spree/admin/payment_methods_controller_decorator.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index 2e5dfa19fd..7941deb9ee 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -45,6 +45,15 @@ module Spree end private + + def load_data + if spree_current_user.admin? || Rails.env.test? + @providers = Gateway.providers.sort{|p1, p2| p1.name <=> p2.name } + else + @providers = Gateway.providers.reject{ |p| p.name.include? "Bogus" }.sort{|p1, p2| p1.name <=> p2.name } + end + end + def load_hubs @hubs = Enterprise.managed_by(spree_current_user).is_distributor.sort_by!{ |d| [(@payment_method.has_distributor? d) ? 0 : 1, d.name] } end From cc011f51364bf1cbca172e31e65c195dc1637da7 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 12 Aug 2014 12:34:42 +1000 Subject: [PATCH 131/205] Switch to forked version of better_spree_paypal_express - passes customer email and phone number to paypal. Waiting on PR #117 --- Gemfile | 6 +++++- Gemfile.lock | 18 +++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 0ed6cfc3f3..456234f554 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,11 @@ gem 'pg' gem 'spree', :github => 'openfoodfoundation/spree', :branch => '1-3-stable' gem 'spree_i18n', :github => 'spree/spree_i18n' gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '1-3-stable' -gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable" + +# Waiting on merge of PR #117 +# https://github.com/spree-contrib/better_spree_paypal_express/pull/117 +gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "1-3-stable" +#gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable" gem 'comfortable_mexican_sofa' diff --git a/Gemfile.lock b/Gemfile.lock index 4a5a94f0a8..9bb6e501db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,6 +12,15 @@ GIT specs: custom_error_message (1.1.1) +GIT + remote: git://github.com/openfoodfoundation/better_spree_paypal_express.git + revision: cdd61161ccd27cd8d183f9321422c7be113796b8 + branch: 1-3-stable + specs: + spree_paypal_express (2.0.3) + paypal-sdk-merchant (= 1.106.1) + spree_core (~> 1.3.4) + GIT remote: git://github.com/openfoodfoundation/spree.git revision: bbe5e779bcb883a1726ad4006d7c06b06c3f5372 @@ -54,15 +63,6 @@ GIT spree_sample (1.3.6.beta) spree_core (= 1.3.6.beta) -GIT - remote: git://github.com/spree-contrib/better_spree_paypal_express.git - revision: db135b89a289aaab951c1228bcc55871de0cbba7 - branch: 1-3-stable - specs: - spree_paypal_express (2.0.3) - paypal-sdk-merchant (= 1.106.1) - spree_core (~> 1.3.4) - GIT remote: git://github.com/spree/deface.git revision: 1110a1336252109bce7f98f9182042e0bc2930ae From d05e5e430ac55588d4a8ff2a71f60e45b5a9d1ea Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 12 Aug 2014 14:06:13 +1000 Subject: [PATCH 132/205] Revert "Adding CTA button for crowdfunding site to homepage & countdown timer directive" This reverts commit 75f3358e2a52290a304bc7bb12b92966fcabbe86. --- app/assets/javascripts/darkswarm/all.js.coffee | 1 - .../javascripts/darkswarm/darkswarm.js.coffee | 1 - .../stylesheets/darkswarm/home_tagline.css.sass | 11 +++-------- app/views/home/index.html.haml | 14 ++++---------- vendor/assets/javascripts/angular-timer.min.js | 8 -------- 5 files changed, 7 insertions(+), 28 deletions(-) delete mode 100755 vendor/assets/javascripts/angular-timer.min.js diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 45f91366ba..f529ac3255 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -11,7 +11,6 @@ #= require lodash.underscore.js #= require angular-scroll.min.js #= require angular-google-maps.min.js -#= require angular-timer.min.js #= require ../shared/mm-foundation-tpls-0.2.2.min.js #= require ../shared/bindonce.min.js #= require ../shared/ng-infinite-scroll.min.js diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index 7a6cba7070..1e58fe7294 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -5,7 +5,6 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", 'infinite-scroll', 'angular-flash.service', 'templates', - 'timer', 'ngSanitize', 'ngAnimate', 'google-maps', diff --git a/app/assets/stylesheets/darkswarm/home_tagline.css.sass b/app/assets/stylesheets/darkswarm/home_tagline.css.sass index ce8b275747..ed85e07d60 100644 --- a/app/assets/stylesheets/darkswarm/home_tagline.css.sass +++ b/app/assets/stylesheets/darkswarm/home_tagline.css.sass @@ -7,24 +7,19 @@ background-color: black background-image: url("/assets/home/ofn_bg_1.jpg") @include fullbg - height: 500px + height: 400px padding: 40px 0px - h1, h2, span, small, timer + h1, h2, p color: white - p - color: $clr-brick-light h1 - margin-bottom: 3rem + margin-bottom: 1em h2 font-size: 1.6875rem max-width: 610px margin: 0 auto - padding-bottom: 0.5rem a color: white &:hover, &:active, &:focus color: $clr-brick-light-bright @include textsoftpress - a.button.primary - color: white \ No newline at end of file diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index cd220f3d53..63d2216052 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -2,16 +2,10 @@ .row .small-12.text-center.columns %h1= image_tag "ofn_logo_beta.png", title: "Open Food Network (beta)" - %h2 We're crowdfunding right now! - %h5 - %timer{"end-time" => '1407679200000'} - {{days}} days, {{hours}} hrs, {{minutes}} mins & {{seconds}} secs to go - %p Help us make Open Food Network the best it can be: - %a.button.primary{href: "http://startsomegood.com/openfoodnetwork", target:"_blank"} Support now - / %h2 An open marketplace that makes it easy to find, buy, sell and move sustainable local food. - %br - %ofn-modal{title: "Learn more"} - = render partial: "modals/learn_more" + %h2 An open marketplace that makes it easy to find, buy, sell and move sustainable local food. + + %ofn-modal{title: "Learn more"} + = render partial: "modals/learn_more" = render partial: "home/hubs" diff --git a/vendor/assets/javascripts/angular-timer.min.js b/vendor/assets/javascripts/angular-timer.min.js deleted file mode 100755 index 9fdc966a93..0000000000 --- a/vendor/assets/javascripts/angular-timer.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * angular-timer - v1.1.6 - 2014-07-01 7:37 AM - * https://github.com/siddii/angular-timer - * - * Copyright (c) 2014 Siddique Hameed - * Licensed MIT - */ -var timerModule=angular.module("timer",[]).directive("timer",["$compile",function(a){return{restrict:"EAC",replace:!1,scope:{interval:"=interval",startTimeAttr:"=startTime",endTimeAttr:"=endTime",countdownattr:"=countdown",finishCallback:"&finishCallback",autoStart:"&autoStart",maxTimeUnit:"="},controller:["$scope","$element","$attrs","$timeout",function(b,c,d,e){function f(){b.timeoutId&&clearTimeout(b.timeoutId)}function g(){b.maxTimeUnit&&"day"!==b.maxTimeUnit?"second"===b.maxTimeUnit?(b.seconds=Math.floor(b.millis/1e3),b.minutes=0,b.hours=0,b.days=0,b.months=0,b.years=0):"minute"===b.maxTimeUnit?(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4),b.hours=0,b.days=0,b.months=0,b.years=0):"hour"===b.maxTimeUnit?(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4%60),b.hours=Math.floor(b.millis/36e5),b.days=0,b.months=0,b.years=0):"month"===b.maxTimeUnit?(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4%60),b.hours=Math.floor(b.millis/36e5%24),b.days=Math.floor(b.millis/36e5/24%30),b.months=Math.floor(b.millis/36e5/24/30),b.years=0):"year"===b.maxTimeUnit&&(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4%60),b.hours=Math.floor(b.millis/36e5%24),b.days=Math.floor(b.millis/36e5/24%30),b.months=Math.floor(b.millis/36e5/24/30%12),b.years=Math.floor(b.millis/36e5/24/365)):(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4%60),b.hours=Math.floor(b.millis/36e5%24),b.days=Math.floor(b.millis/36e5/24),b.months=0,b.years=0),b.secondsS=1==b.seconds?"":"s",b.minutesS=1==b.minutes?"":"s",b.hoursS=1==b.hours?"":"s",b.daysS=1==b.days?"":"s",b.monthsS=1==b.months?"":"s",b.yearsS=1==b.years?"":"s",b.sseconds=b.seconds<10?"0"+b.seconds:b.seconds,b.mminutes=b.minutes<10?"0"+b.minutes:b.minutes,b.hhours=b.hours<10?"0"+b.hours:b.hours,b.ddays=b.days<10?"0"+b.days:b.days,b.mmonths=b.months<10?"0"+b.months:b.months,b.yyears=b.years<10?"0"+b.years:b.years}"function"!=typeof String.prototype.trim&&(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),b.autoStart=d.autoStart||d.autostart,c.append(0===c.html().trim().length?a("{{millis}}")(b):a(c.contents())(b)),b.startTime=null,b.endTime=null,b.timeoutId=null,b.countdown=b.countdownattr&&parseInt(b.countdownattr,10)>=0?parseInt(b.countdownattr,10):void 0,b.isRunning=!1,b.$on("timer-start",function(){b.start()}),b.$on("timer-resume",function(){b.resume()}),b.$on("timer-stop",function(){b.stop()}),b.$on("timer-clear",function(){b.clear()}),b.$on("timer-set-countdown",function(a,c){b.countdown=c}),b.start=c[0].start=function(){b.startTime=b.startTimeAttr?new Date(b.startTimeAttr):new Date,b.endTime=b.endTimeAttr?new Date(b.endTimeAttr):null,b.countdown||(b.countdown=b.countdownattr&&parseInt(b.countdownattr,10)>0?parseInt(b.countdownattr,10):void 0),f(),h(),b.isRunning=!0},b.resume=c[0].resume=function(){f(),b.countdownattr&&(b.countdown+=1),b.startTime=new Date-(b.stoppedTime-b.startTime),h(),b.isRunning=!0},b.stop=b.pause=c[0].stop=c[0].pause=function(){var a=b.timeoutId;b.clear(),b.$emit("timer-stopped",{timeoutId:a,millis:b.millis,seconds:b.seconds,minutes:b.minutes,hours:b.hours,days:b.days})},b.clear=c[0].clear=function(){b.stoppedTime=new Date,f(),b.timeoutId=null,b.isRunning=!1},c.bind("$destroy",function(){f(),b.isRunning=!1}),b.countdownattr?(b.millis=1e3*b.countdownattr,b.addCDSeconds=c[0].addCDSeconds=function(a){b.countdown+=a,b.$digest(),b.isRunning||b.start()},b.$on("timer-add-cd-seconds",function(a,c){e(function(){b.addCDSeconds(c)})}),b.$on("timer-set-countdown-seconds",function(a,c){b.isRunning||b.clear(),b.countdown=c,b.millis=1e3*c,g()})):b.millis=0,g();var h=function(){b.millis=new Date-b.startTime;var a=b.millis%1e3;return b.endTimeAttr&&(b.millis=b.endTime-new Date,a=b.interval-b.millis%1e3),b.countdownattr&&(b.millis=1e3*b.countdown),b.millis<0?(b.stop(),b.millis=0,g(),void(b.finishCallback&&b.$eval(b.finishCallback))):(g(),b.timeoutId=setTimeout(function(){h(),b.$digest()},b.interval-a),b.$emit("timer-tick",{timeoutId:b.timeoutId,millis:b.millis}),void(b.countdown>0?b.countdown--:b.countdown<=0&&(b.stop(),b.finishCallback&&b.$eval(b.finishCallback))))};(void 0===b.autoStart||b.autoStart===!0)&&b.start()}]}}]);"undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports=timerModule); \ No newline at end of file From f30c67da7b74916d1ca0fb48534fb102bfd25bf0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 12 Aug 2014 14:07:16 +1000 Subject: [PATCH 133/205] Remove crowdfunding notice from README --- README.markdown | 1 - 1 file changed, 1 deletion(-) diff --git a/README.markdown b/README.markdown index 8975711f47..6ffde4df5c 100644 --- a/README.markdown +++ b/README.markdown @@ -9,7 +9,6 @@ Supported by the Open Food Foundation, we are proudly open source and not-for-pr We're part of global movement - get involved! -* We're crowd-funding RIGHT NOW - please help out at http://startsomegood.com/openfoodnetwork * Fill in this short survey to tell us who you are and what you want to do with OFN: https://docs.google.com/a/eaterprises.com.au/forms/d/1zxR5vSiU9CigJ9cEaC8-eJLgYid8CR8er7PPH9Mc-30/edit# * Find out more and join in the conversation - http://openfoodnetwork.org From 50b8eaecab69340961f198cc9b9240f6ffe6bbbb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 13 Aug 2014 15:07:32 +1000 Subject: [PATCH 134/205] Add fundraising fees --- app/assets/javascripts/templates/price_breakdown.html.haml | 3 +++ app/models/enterprise_fee.rb | 2 +- .../open_food_network/enterprise_fee_calculator_spec.rb | 7 ++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index 01588933ca..0b7eba917a 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -24,6 +24,9 @@ %li{"bo-if" => "variant.fees.transport"} .right {{ variant.fees.transport | currency }} Transport fee + %li{"bo-if" => "variant.fees.fundraising"} + .right {{ variant.fees.fundraising | currency }} + Fundraising fee %li %strong .right = {{ variant.price | currency }} diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index f41ad76c96..6270f77b7a 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -10,7 +10,7 @@ class EnterpriseFee < ActiveRecord::Base attr_accessible :enterprise_id, :fee_type, :name, :calculator_type - FEE_TYPES = %w(packing transport admin sales) + FEE_TYPES = %w(packing transport admin sales fundraising) PER_ORDER_CALCULATORS = ['Spree::Calculator::FlatRate', 'Spree::Calculator::FlexiRate'] diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb index 0ca7da4999..9c950234cd 100644 --- a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -37,19 +37,20 @@ module OpenFoodNetwork let!(:ef_sales) { create(:enterprise_fee, fee_type: 'sales', amount: 4.56) } let!(:ef_packing) { create(:enterprise_fee, fee_type: 'packing', amount: 7.89) } let!(:ef_transport) { create(:enterprise_fee, fee_type: 'transport', amount: 0.12) } + let!(:ef_fundraising) { create(:enterprise_fee, fee_type: 'fundraising', amount: 3.45) } let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, - enterprise_fees: [ef_admin, ef_sales, ef_packing, ef_transport], + enterprise_fees: [ef_admin, ef_sales, ef_packing, ef_transport, ef_fundraising], variants: [product.master]) } it "returns a breakdown of fees" do - EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12} + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} end it "filters out zero fees" do ef_admin.calculator.update_attribute :preferred_amount, 0 - EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {sales: 4.56, packing: 7.89, transport: 0.12} + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} end end From 3d2c19623739cf020f72480899235bda0b2d7bb7 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 14 Aug 2014 12:09:23 +1000 Subject: [PATCH 135/205] Tweak layout for checkout accordions to prevent expand and hide from dropping over two lines on small devices --- app/views/checkout/_billing.html.haml | 4 ++-- app/views/checkout/_details.html.haml | 4 ++-- app/views/checkout/_payment.html.haml | 4 ++-- app/views/checkout/_shipping.html.haml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index 5ec2f12208..2fceedb523 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -13,11 +13,11 @@ "ng-class" => "{valid: billing.$valid, open: accordion.billing}"} %accordion-heading .row - .small-10.columns + .small-8.medium-10.columns %em %small {{ summary() | printArray }} - .small-2.columns.text-right + .small-4.medium-2.columns.text-right %span.accordion-up %em %small Hide diff --git a/app/views/checkout/_details.html.haml b/app/views/checkout/_details.html.haml index e3b369182f..6bb24d4682 100644 --- a/app/views/checkout/_details.html.haml +++ b/app/views/checkout/_details.html.haml @@ -13,11 +13,11 @@ "ng-class" => "{valid: details.$valid, open: accordion.details}"} %accordion-heading .row - .small-10.columns + .small-8.medium-10.columns %em %small {{ summary() | printArray }} - .small-2.columns.text-right + .small-4.medium-2.columns.text-right %span.accordion-up %em %small Hide diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index 48c5e15711..e6a0fd373c 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -13,11 +13,11 @@ "ng-class" => "{valid: payment.$valid, open: accordion.payment}"} %accordion-heading .row - .small-10.columns + .small-8.medium-10.columns %em %small {{ Checkout.paymentMethod().name }} - .small-2.columns.text-right + .small-4.medium-2.columns.text-right %span.accordion-up %em %small Hide diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index 7906824a36..8904facff4 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -13,11 +13,11 @@ "ng-class" => "{valid: shipping.$valid, open: accordion.shipping}"} %accordion-heading .row - .small-10.columns + .small-8.medium-10.columns %em %small {{ Checkout.shippingMethod().name }} - .small-2.columns.text-right + .small-4.medium-2.columns.text-right %span.accordion-up %em %small Hide From 8ef91d1b2a9724c94fa92ede698dd0e31e1cfd17 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 14 Aug 2014 12:09:55 +1000 Subject: [PATCH 136/205] Styling and layout tweaks for checkout page to improve how columns sit on small devices --- app/assets/stylesheets/darkswarm/checkout.css.sass | 9 +++++++++ app/views/checkout/edit.html.haml | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass index e9a7f09597..a592c5dc25 100644 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -3,10 +3,19 @@ checkout display: block + + @media all and (max-width: 640px) + &.row .row + margin-left: 0 + margin-right: 0 orderdetails .button, table width: 100% + @media all and (max-width: 640px) + form.edit_order + border: 1px solid $disabled-bright + margin-bottom: 2rem #details, #billing, #shipping, #payment border: 0 diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index 1c4932c481..7fffdbeb22 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -13,12 +13,12 @@ %accordion{"close-others" => "true"} %checkout.row{"ng-controller" => "CheckoutCtrl"} - .small-9.columns + .small-12.medium-8.large-9.columns - unless spree_current_user = render partial: "checkout/authentication" .row{"ng-show" => "enabled", "ng-controller" => "AccordionCtrl"} = render partial: "checkout/form" - .small-3.columns + .small-12.medium-4.large-3.columns = render partial: "checkout/summary" From 7f6a7b4254f529c5c86f61892810e16bdecd7676 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 15 Aug 2014 11:02:21 +1000 Subject: [PATCH 137/205] Adding underlines to links on modals for map view etc to try and make these more intuitive and usable --- .../javascripts/templates/partials/contact.html.haml | 2 +- .../templates/partials/enterprise_header.html.haml | 4 ++-- .../stylesheets/darkswarm/modal-enterprises.css.sass | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/templates/partials/contact.html.haml b/app/assets/javascripts/templates/partials/contact.html.haml index 78828984c4..caa3bc5e65 100644 --- a/app/assets/javascripts/templates/partials/contact.html.haml +++ b/app/assets/javascripts/templates/partials/contact.html.haml @@ -1,4 +1,4 @@ -%div{bindonce: true} +%div.contact-container{bindonce: true} %div.modal-centered{"bo-if" => "enterprise.email || enterprise.website || enterprise.phone"} %p.modal-header Contact %p{"ng-if" => "enterprise.phone"} diff --git a/app/assets/javascripts/templates/partials/enterprise_header.html.haml b/app/assets/javascripts/templates/partials/enterprise_header.html.haml index cb41670831..96a4fb207e 100644 --- a/app/assets/javascripts/templates/partials/enterprise_header.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_header.html.haml @@ -5,8 +5,8 @@ %h3{"ng-if" => "enterprise.is_distributor"} %a{"bo-href" => "enterprise.path", "ofn-empties-cart" => "enterprise", bindonce: true} %i.ofn-i_040-hub - {{ enterprise.name }} + %span {{ enterprise.name }} %h3{"ng-if" => "!enterprise.is_distributor"} %i.ofn-i_036-producers - {{ enterprise.name }} + %span {{ enterprise.name }} %img.hero-img{"ng-src" => "{{enterprise.promo_image}}"} diff --git a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass index 837fd8e08a..4883aa3572 100644 --- a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass +++ b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass @@ -22,6 +22,9 @@ p line-height: 2 + h3 a:hover span + border-bottom: 1px solid $clr-brick-bright + // ABOUT Enterprise @@ -41,6 +44,11 @@ max-width: 180px max-height: 180px +// CONTACT Enterprise + +.contact-container + a:hover + text-decoration: underline // FOLLOW Enterprise From 1d77812ab8ecf488778f57346c51972529781c54 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 13 Aug 2014 16:49:45 +1000 Subject: [PATCH 138/205] Admin can list enterprise roles --- .../enterprise_roles_controller.js.coffee | 9 ++++ .../admin/services/enterprise_roles.js.coffee | 18 ++++++++ .../admin/enterprise_roles_controller.rb | 7 +++ app/models/enterprise_role.rb | 2 + .../index/add_roles_link.html.haml.deface | 3 ++ .../admin/enterprise_roles/_data.html.haml | 2 + .../_enterprise_role.html.haml | 5 +++ .../admin/enterprise_roles/index.html.haml | 15 +++++++ app/views/admin/json/_enterprise_role.rabl | 11 +++++ app/views/admin/json/_enterprise_roles.rabl | 2 + .../shared/_enterprises_sub_menu.html.haml | 2 +- .../admin/shared/_users_sub_menu.html.haml | 4 ++ config/routes.rb | 1 + spec/factories.rb | 3 ++ spec/features/admin/enterprise_roles_spec.rb | 43 +++++++++++++++++++ 15 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee create mode 100644 app/assets/javascripts/admin/services/enterprise_roles.js.coffee create mode 100644 app/controllers/admin/enterprise_roles_controller.rb create mode 100644 app/overrides/spree/admin/users/index/add_roles_link.html.haml.deface create mode 100644 app/views/admin/enterprise_roles/_data.html.haml create mode 100644 app/views/admin/enterprise_roles/_enterprise_role.html.haml create mode 100644 app/views/admin/enterprise_roles/index.html.haml create mode 100644 app/views/admin/json/_enterprise_role.rabl create mode 100644 app/views/admin/json/_enterprise_roles.rabl create mode 100644 app/views/admin/shared/_users_sub_menu.html.haml create mode 100644 spec/features/admin/enterprise_roles_spec.rb diff --git a/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee new file mode 100644 index 0000000000..b6139a523c --- /dev/null +++ b/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee @@ -0,0 +1,9 @@ +angular.module("ofn.admin").controller "AdminEnterpriseRolesCtrl", ($scope, EnterpriseRoles) -> + $scope.EnterpriseRoles = EnterpriseRoles + + $scope.create = -> + $scope.EnterpriseRoles.create($scope.user_id, $scope.enterprise_id) + + $scope.delete = (enterprise_role) -> + if confirm("Are you sure?") + $scope.EnterpriseRoles.delete enterprise_role diff --git a/app/assets/javascripts/admin/services/enterprise_roles.js.coffee b/app/assets/javascripts/admin/services/enterprise_roles.js.coffee new file mode 100644 index 0000000000..9ff49ad7fe --- /dev/null +++ b/app/assets/javascripts/admin/services/enterprise_roles.js.coffee @@ -0,0 +1,18 @@ +angular.module("ofn.admin").factory 'EnterpriseRoles', ($http, enterprise_roles) -> + new class EnterpriseRoles + create_errors: "" + + constructor: -> + @enterprise_roles = enterprise_roles + + create: (user_id, enterprise_id) -> + $http.post('/admin/enterprise_roles', {enterprise_role: {user_id: user_id, enterprise_id: enterprise_id}}).success (data, status) => + @enterprise_roles.unshift(data) + @create_errors = "" + + .error (response, status) => + @create_errors = response.errors + + delete: (er) -> + $http.delete('/admin/enterprise_roles/' + er.id).success (data) => + @enterprise_roles.splice @enterprise_roles.indexOf(er), 1 diff --git a/app/controllers/admin/enterprise_roles_controller.rb b/app/controllers/admin/enterprise_roles_controller.rb new file mode 100644 index 0000000000..40f2b9ff4d --- /dev/null +++ b/app/controllers/admin/enterprise_roles_controller.rb @@ -0,0 +1,7 @@ +module Admin + class EnterpriseRolesController < ResourceController + def index + @enterprise_roles = EnterpriseRole.by_user_email + end + end +end diff --git a/app/models/enterprise_role.rb b/app/models/enterprise_role.rb index 94b926f84f..f853e057b6 100644 --- a/app/models/enterprise_role.rb +++ b/app/models/enterprise_role.rb @@ -1,4 +1,6 @@ class EnterpriseRole < ActiveRecord::Base belongs_to :user, :class_name => Spree.user_class belongs_to :enterprise + + scope :by_user_email, joins(:user).order('spree_users.email ASC') end diff --git a/app/overrides/spree/admin/users/index/add_roles_link.html.haml.deface b/app/overrides/spree/admin/users/index/add_roles_link.html.haml.deface new file mode 100644 index 0000000000..f99a1ebff4 --- /dev/null +++ b/app/overrides/spree/admin/users/index/add_roles_link.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_before "table#listing_users" + += render 'admin/shared/users_sub_menu' \ No newline at end of file diff --git a/app/views/admin/enterprise_roles/_data.html.haml b/app/views/admin/enterprise_roles/_data.html.haml new file mode 100644 index 0000000000..3c4003a2f0 --- /dev/null +++ b/app/views/admin/enterprise_roles/_data.html.haml @@ -0,0 +1,2 @@ +:javascript + angular.module('ofn.admin').value('enterprise_roles', #{render partial: "admin/json/enterprise_roles", object: @enterprise_roles}); diff --git a/app/views/admin/enterprise_roles/_enterprise_role.html.haml b/app/views/admin/enterprise_roles/_enterprise_role.html.haml new file mode 100644 index 0000000000..5e4986d599 --- /dev/null +++ b/app/views/admin/enterprise_roles/_enterprise_role.html.haml @@ -0,0 +1,5 @@ +%td {{ enterprise_role.user_email }} +%td manages +%td {{ enterprise_role.enterprise_name }} +%td.actions + %a.delete-enterprise-role.icon-trash.no-text{'ng-click' => 'delete(enterprise_role)'} diff --git a/app/views/admin/enterprise_roles/index.html.haml b/app/views/admin/enterprise_roles/index.html.haml new file mode 100644 index 0000000000..f7060d2fc0 --- /dev/null +++ b/app/views/admin/enterprise_roles/index.html.haml @@ -0,0 +1,15 @@ +- content_for :page_title do + Roles + += render 'admin/shared/users_sub_menu' + +%div{"ng-app" => "ofn.admin", "ng-controller" => "AdminEnterpriseRolesCtrl"} + = render 'data' + + %input.search{"ng-model" => "query", "placeholder" => "Search"} + + %table#enterprise-roles + %tbody + -#= render 'form' + %tr{"ng-repeat" => "enterprise_role in EnterpriseRoles.enterprise_roles | filter:query"} + = render 'enterprise_role' diff --git a/app/views/admin/json/_enterprise_role.rabl b/app/views/admin/json/_enterprise_role.rabl new file mode 100644 index 0000000000..c5836e35e4 --- /dev/null +++ b/app/views/admin/json/_enterprise_role.rabl @@ -0,0 +1,11 @@ +object @enterprise_role + +attributes :id, :user_id, :enterprise_id + +node :user_email do |enterprise_role| + enterprise_role.user.email +end + +node :enterprise_name do |enterprise_role| + enterprise_role.enterprise.name +end diff --git a/app/views/admin/json/_enterprise_roles.rabl b/app/views/admin/json/_enterprise_roles.rabl new file mode 100644 index 0000000000..3fb82fdca8 --- /dev/null +++ b/app/views/admin/json/_enterprise_roles.rabl @@ -0,0 +1,2 @@ +collection @enterprise_roles +extends "admin/json/enterprise_role" diff --git a/app/views/admin/shared/_enterprises_sub_menu.html.haml b/app/views/admin/shared/_enterprises_sub_menu.html.haml index 4a793689b7..31366091ef 100644 --- a/app/views/admin/shared/_enterprises_sub_menu.html.haml +++ b/app/views/admin/shared/_enterprises_sub_menu.html.haml @@ -1,4 +1,4 @@ = content_for :sub_menu do - %ul#sub_nav.inline-menu{"data-hook" => "admin_order_sub_tabs"} + %ul#sub_nav.inline-menu{"data-hook" => "admin_enterprise_sub_tabs"} = tab :enterprises, url: main_app.admin_enterprises_path = tab :relationships, url: main_app.admin_enterprise_relationships_path, match_path: '/enterprise_relationships' diff --git a/app/views/admin/shared/_users_sub_menu.html.haml b/app/views/admin/shared/_users_sub_menu.html.haml new file mode 100644 index 0000000000..a5911277a6 --- /dev/null +++ b/app/views/admin/shared/_users_sub_menu.html.haml @@ -0,0 +1,4 @@ += content_for :sub_menu do + %ul#sub_nav.inline-menu{"data-hook" => "admin_user_sub_tabs"} + = tab :users, url: spree.admin_users_path + = tab :roles, url: main_app.admin_enterprise_roles_path, match_path: '/enterprise_roles' diff --git a/config/routes.rb b/config/routes.rb index b9c0a2da55..36213b2622 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,7 @@ Openfoodnetwork::Application.routes.draw do end resources :enterprise_relationships + resources :enterprise_roles resources :enterprise_fees do post :bulk_update, :on => :collection, :as => :bulk_update diff --git a/spec/factories.rb b/spec/factories.rb index 64afa6a21a..7102229cff 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -102,6 +102,9 @@ FactoryGirl.define do factory :enterprise_relationship do end + factory :enterprise_role do + end + factory :enterprise_group, :class => EnterpriseGroup do name 'Enterprise group' description 'this is a group' diff --git a/spec/features/admin/enterprise_roles_spec.rb b/spec/features/admin/enterprise_roles_spec.rb new file mode 100644 index 0000000000..44f0b96132 --- /dev/null +++ b/spec/features/admin/enterprise_roles_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +feature %q{ + As an Administrator + I want to manage relationships between users and enterprises +}, js: true do + include AuthenticationWorkflow + include WebHelper + + + context "as a site administrator" do + before { login_to_admin_section } + + scenario "listing relationships" do + # Given some users and enterprises with relationships + u1, u2 = create(:user), create(:user) + e1, e2, e3, e4 = create(:enterprise), create(:enterprise), create(:enterprise), create(:enterprise) + create(:enterprise_role, user: u1, enterprise: e1) + create(:enterprise_role, user: u1, enterprise: e2) + create(:enterprise_role, user: u2, enterprise: e3) + create(:enterprise_role, user: u2, enterprise: e4) + + # When I go to the roles page + click_link 'Users' + click_link 'Roles' + + # Then I should see the relationships + within('table#enterprise-roles') do + page.should have_relationship u1, e1 + page.should have_relationship u1, e2 + page.should have_relationship u2, e3 + page.should have_relationship u2, e4 + end + end + end + + + private + + def have_relationship(user, enterprise) + have_table_row [user.email, 'manages', enterprise.name, ''] + end +end From 8bc9def6ae70f5d0e70ae4b42a2e0dad0f835962 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 Aug 2014 11:50:49 +1000 Subject: [PATCH 139/205] Switch to AMS for enterprise role serialisation --- .../admin/services/enterprise_roles.js.coffee | 4 ++-- app/helpers/admin/injection_helper.rb | 6 +++++- .../api/admin/enterprise_role_serializer.rb | 11 +++++++++++ app/views/admin/enterprise_roles/_data.html.haml | 3 +-- app/views/admin/json/_enterprise_role.rabl | 11 ----------- app/views/admin/json/_enterprise_roles.rabl | 2 -- 6 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 app/serializers/api/admin/enterprise_role_serializer.rb delete mode 100644 app/views/admin/json/_enterprise_role.rabl delete mode 100644 app/views/admin/json/_enterprise_roles.rabl diff --git a/app/assets/javascripts/admin/services/enterprise_roles.js.coffee b/app/assets/javascripts/admin/services/enterprise_roles.js.coffee index 9ff49ad7fe..ca11ab33c6 100644 --- a/app/assets/javascripts/admin/services/enterprise_roles.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_roles.js.coffee @@ -1,9 +1,9 @@ -angular.module("ofn.admin").factory 'EnterpriseRoles', ($http, enterprise_roles) -> +angular.module("ofn.admin").factory 'EnterpriseRoles', ($http, enterpriseRoles) -> new class EnterpriseRoles create_errors: "" constructor: -> - @enterprise_roles = enterprise_roles + @enterprise_roles = enterpriseRoles create: (user_id, enterprise_id) -> $http.post('/admin/enterprise_roles', {enterprise_role: {user_id: user_id, enterprise_id: enterprise_id}}).success (data, status) => diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 481e7912c6..7ded0cc18a 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -4,6 +4,10 @@ module Admin admin_inject_json_ams "admin.enterprises", "enterprise", @enterprise, Api::Admin::EnterpriseSerializer end + def admin_inject_enterprise_roles + admin_inject_json_ams_array "ofn.admin", "enterpriseRoles", @enterprise_roles, Api::Admin::EnterpriseRoleSerializer + end + def admin_inject_payment_methods admin_inject_json_ams_array "admin.payment_methods", "paymentMethods", @payment_methods, Api::Admin::IdNameSerializer end @@ -30,4 +34,4 @@ module Admin render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} end end -end \ No newline at end of file +end diff --git a/app/serializers/api/admin/enterprise_role_serializer.rb b/app/serializers/api/admin/enterprise_role_serializer.rb new file mode 100644 index 0000000000..3b5f889f8c --- /dev/null +++ b/app/serializers/api/admin/enterprise_role_serializer.rb @@ -0,0 +1,11 @@ +class Api::Admin::EnterpriseRoleSerializer < ActiveModel::Serializer + attributes :id, :user_id, :enterprise_id, :user_email, :enterprise_name + + def user_email + object.user.email + end + + def enterprise_name + object.enterprise.name + end +end diff --git a/app/views/admin/enterprise_roles/_data.html.haml b/app/views/admin/enterprise_roles/_data.html.haml index 3c4003a2f0..b68cff2cb7 100644 --- a/app/views/admin/enterprise_roles/_data.html.haml +++ b/app/views/admin/enterprise_roles/_data.html.haml @@ -1,2 +1 @@ -:javascript - angular.module('ofn.admin').value('enterprise_roles', #{render partial: "admin/json/enterprise_roles", object: @enterprise_roles}); += admin_inject_enterprise_roles diff --git a/app/views/admin/json/_enterprise_role.rabl b/app/views/admin/json/_enterprise_role.rabl deleted file mode 100644 index c5836e35e4..0000000000 --- a/app/views/admin/json/_enterprise_role.rabl +++ /dev/null @@ -1,11 +0,0 @@ -object @enterprise_role - -attributes :id, :user_id, :enterprise_id - -node :user_email do |enterprise_role| - enterprise_role.user.email -end - -node :enterprise_name do |enterprise_role| - enterprise_role.enterprise.name -end diff --git a/app/views/admin/json/_enterprise_roles.rabl b/app/views/admin/json/_enterprise_roles.rabl deleted file mode 100644 index 3fb82fdca8..0000000000 --- a/app/views/admin/json/_enterprise_roles.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @enterprise_roles -extends "admin/json/enterprise_role" From 6e17f0aaa251620597850b182576e5c38670a66a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 Aug 2014 12:41:12 +1000 Subject: [PATCH 140/205] Admin can create enterprise roles --- .../enterprise_roles_controller.js.coffee | 4 +++- .../javascripts/admin/services/users.js.coffee | 4 ++++ .../admin/enterprise_roles_controller.rb | 13 +++++++++++++ app/helpers/admin/injection_helper.rb | 12 ++++++++++++ app/serializers/api/admin/user_serializer.rb | 3 +++ app/views/admin/enterprise_roles/_data.html.haml | 3 +++ app/views/admin/enterprise_roles/_form.html.haml | 9 +++++++++ app/views/admin/enterprise_roles/index.html.haml | 2 +- spec/features/admin/enterprise_roles_spec.rb | 13 +++++++++++++ 9 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/admin/services/users.js.coffee create mode 100644 app/serializers/api/admin/user_serializer.rb create mode 100644 app/views/admin/enterprise_roles/_form.html.haml diff --git a/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee index b6139a523c..026913a263 100644 --- a/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee @@ -1,5 +1,7 @@ -angular.module("ofn.admin").controller "AdminEnterpriseRolesCtrl", ($scope, EnterpriseRoles) -> +angular.module("ofn.admin").controller "AdminEnterpriseRolesCtrl", ($scope, EnterpriseRoles, Users, Enterprises) -> $scope.EnterpriseRoles = EnterpriseRoles + $scope.Users = Users + $scope.Enterprises = Enterprises $scope.create = -> $scope.EnterpriseRoles.create($scope.user_id, $scope.enterprise_id) diff --git a/app/assets/javascripts/admin/services/users.js.coffee b/app/assets/javascripts/admin/services/users.js.coffee new file mode 100644 index 0000000000..638908542e --- /dev/null +++ b/app/assets/javascripts/admin/services/users.js.coffee @@ -0,0 +1,4 @@ +angular.module("ofn.admin").factory 'Users', (users) -> + new class Users + constructor: -> + @users = users diff --git a/app/controllers/admin/enterprise_roles_controller.rb b/app/controllers/admin/enterprise_roles_controller.rb index 40f2b9ff4d..9ce3eafcab 100644 --- a/app/controllers/admin/enterprise_roles_controller.rb +++ b/app/controllers/admin/enterprise_roles_controller.rb @@ -2,6 +2,19 @@ module Admin class EnterpriseRolesController < ResourceController def index @enterprise_roles = EnterpriseRole.by_user_email + @users = Spree::User.order('spree_users.email') + @my_enterprises = @all_enterprises = Enterprise.by_name + end + + def create + @enterprise_role = EnterpriseRole.new params[:enterprise_role] + + if @enterprise_role.save + render text: Api::Admin::EnterpriseRoleSerializer.new(@enterprise_role).to_json + + else + render status: 400, json: {errors: @enterprise_role.errors.full_messages.join(', ')} + end end end end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 7ded0cc18a..60bc911087 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -4,6 +4,11 @@ module Admin admin_inject_json_ams "admin.enterprises", "enterprise", @enterprise, Api::Admin::EnterpriseSerializer end + def admin_inject_enterprises + admin_inject_json_ams_array("ofn.admin", "my_enterprises", @my_enterprises, Api::Admin::EnterpriseSerializer) + + admin_inject_json_ams_array("ofn.admin", "all_enterprises", @all_enterprises, Api::Admin::EnterpriseSerializer) + end + def admin_inject_enterprise_roles admin_inject_json_ams_array "ofn.admin", "enterpriseRoles", @enterprise_roles, Api::Admin::EnterpriseRoleSerializer end @@ -24,6 +29,13 @@ module Admin admin_inject_json_ams_array "ofn.admin", "taxons", @taxons, Api::Admin::TaxonSerializer end + def admin_inject_users + admin_inject_json_ams_array "ofn.admin", "users", @users, Api::Admin::UserSerializer + end + + + + def admin_inject_json_ams(ngModule, name, data, serializer, opts = {}) json = serializer.new(data).to_json render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} diff --git a/app/serializers/api/admin/user_serializer.rb b/app/serializers/api/admin/user_serializer.rb new file mode 100644 index 0000000000..501cd75674 --- /dev/null +++ b/app/serializers/api/admin/user_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::UserSerializer < ActiveModel::Serializer + attributes :id, :email +end diff --git a/app/views/admin/enterprise_roles/_data.html.haml b/app/views/admin/enterprise_roles/_data.html.haml index b68cff2cb7..5063503803 100644 --- a/app/views/admin/enterprise_roles/_data.html.haml +++ b/app/views/admin/enterprise_roles/_data.html.haml @@ -1 +1,4 @@ = admin_inject_enterprise_roles += admin_inject_users += admin_inject_enterprises + diff --git a/app/views/admin/enterprise_roles/_form.html.haml b/app/views/admin/enterprise_roles/_form.html.haml new file mode 100644 index 0000000000..2f31e8fa11 --- /dev/null +++ b/app/views/admin/enterprise_roles/_form.html.haml @@ -0,0 +1,9 @@ +%tr + %td + %select{name: "enterprise_role_user_id", "ng-model" => "user_id", "ng-options" => "u.id as u.email for u in Users.users"} + %td manages + %td + %select{name: "enterprise_role_enterprise_id", "ng-model" => "enterprise_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} + %td.actions + %input{type: "button", value: "Create", "ng-click" => "create()"} + .errors {{ EnterpriseRoles.create_errors }} diff --git a/app/views/admin/enterprise_roles/index.html.haml b/app/views/admin/enterprise_roles/index.html.haml index f7060d2fc0..0881a8a50e 100644 --- a/app/views/admin/enterprise_roles/index.html.haml +++ b/app/views/admin/enterprise_roles/index.html.haml @@ -10,6 +10,6 @@ %table#enterprise-roles %tbody - -#= render 'form' + = render 'form' %tr{"ng-repeat" => "enterprise_role in EnterpriseRoles.enterprise_roles | filter:query"} = render 'enterprise_role' diff --git a/spec/features/admin/enterprise_roles_spec.rb b/spec/features/admin/enterprise_roles_spec.rb index 44f0b96132..f8eb51ff01 100644 --- a/spec/features/admin/enterprise_roles_spec.rb +++ b/spec/features/admin/enterprise_roles_spec.rb @@ -32,6 +32,19 @@ feature %q{ page.should have_relationship u2, e4 end end + + scenario "creating a relationship" do + u = create(:user, email: 'u@example.com') + e = create(:enterprise, name: 'One') + + visit admin_enterprise_roles_path + select 'u@example.com', from: 'enterprise_role_user_id' + select 'One', from: 'enterprise_role_enterprise_id' + click_button 'Create' + + page.should have_relationship u, e + EnterpriseRole.where(user_id: u, enterprise_id: e).should be_present + end end From 95a0bf39f75811c24d6cf781d81e7d2194d014ae Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 Aug 2014 15:46:54 +1000 Subject: [PATCH 141/205] Enterprise roles must be unique --- app/models/enterprise_role.rb | 3 +++ ...ue_and_fk_constraints_to_enterprise_roles.rb | 8 ++++++++ db/schema.rb | 3 ++- spec/features/admin/enterprise_roles_spec.rb | 17 +++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20140815053659_add_unique_and_fk_constraints_to_enterprise_roles.rb diff --git a/app/models/enterprise_role.rb b/app/models/enterprise_role.rb index f853e057b6..e3fb116146 100644 --- a/app/models/enterprise_role.rb +++ b/app/models/enterprise_role.rb @@ -2,5 +2,8 @@ class EnterpriseRole < ActiveRecord::Base belongs_to :user, :class_name => Spree.user_class belongs_to :enterprise + validates_presence_of :user_id, :enterprise_id + validates_uniqueness_of :enterprise_id, scope: :user_id, message: "^That role is already present." + scope :by_user_email, joins(:user).order('spree_users.email ASC') end diff --git a/db/migrate/20140815053659_add_unique_and_fk_constraints_to_enterprise_roles.rb b/db/migrate/20140815053659_add_unique_and_fk_constraints_to_enterprise_roles.rb new file mode 100644 index 0000000000..782cad0f20 --- /dev/null +++ b/db/migrate/20140815053659_add_unique_and_fk_constraints_to_enterprise_roles.rb @@ -0,0 +1,8 @@ +class AddUniqueAndFkConstraintsToEnterpriseRoles < ActiveRecord::Migration + def change + add_index :enterprise_roles, [:user_id, :enterprise_id], unique: true + + add_foreign_key :enterprise_roles, :spree_users, column: :user_id + add_foreign_key :enterprise_roles, :enterprises, column: :enterprise_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 84b51e8b93..0ef402ecf5 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 => 20140723023713) do +ActiveRecord::Schema.define(:version => 20140815053659) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -222,6 +222,7 @@ ActiveRecord::Schema.define(:version => 20140723023713) do end add_index "enterprise_roles", ["enterprise_id"], :name => "index_enterprise_roles_on_enterprise_id" + add_index "enterprise_roles", ["user_id", "enterprise_id"], :name => "index_enterprise_roles_on_user_id_and_enterprise_id", :unique => true add_index "enterprise_roles", ["user_id"], :name => "index_enterprise_roles_on_user_id" create_table "enterprises", :force => true do |t| diff --git a/spec/features/admin/enterprise_roles_spec.rb b/spec/features/admin/enterprise_roles_spec.rb index f8eb51ff01..c36810e72a 100644 --- a/spec/features/admin/enterprise_roles_spec.rb +++ b/spec/features/admin/enterprise_roles_spec.rb @@ -45,6 +45,23 @@ feature %q{ page.should have_relationship u, e EnterpriseRole.where(user_id: u, enterprise_id: e).should be_present end + + scenario "attempting to create a relationship with invalid data" do + u = create(:user, email: 'u@example.com') + e = create(:enterprise, name: 'One') + create(:enterprise_role, user: u, enterprise: e) + + expect do + # When I attempt to create a duplicate relationship + visit admin_enterprise_roles_path + select 'u@example.com', from: 'enterprise_role_user_id' + select 'One', from: 'enterprise_role_enterprise_id' + click_button 'Create' + + # Then I should see an error message + page.should have_content "That role is already present." + end.to change(EnterpriseRole, :count).by(0) + end end From 9ddfb1584b0eca79370d1fa21cfcde9f89da5376 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 Aug 2014 16:05:09 +1000 Subject: [PATCH 142/205] Admin can delete enterprise roles --- .../admin/enterprise_roles_controller.rb | 6 ++++++ spec/features/admin/enterprise_roles_spec.rb | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/app/controllers/admin/enterprise_roles_controller.rb b/app/controllers/admin/enterprise_roles_controller.rb index 9ce3eafcab..8ffc2bad01 100644 --- a/app/controllers/admin/enterprise_roles_controller.rb +++ b/app/controllers/admin/enterprise_roles_controller.rb @@ -16,5 +16,11 @@ module Admin render status: 400, json: {errors: @enterprise_role.errors.full_messages.join(', ')} end end + + def destroy + @enterprise_role = EnterpriseRole.find params[:id] + @enterprise_role.destroy + render nothing: true + end end end diff --git a/spec/features/admin/enterprise_roles_spec.rb b/spec/features/admin/enterprise_roles_spec.rb index c36810e72a..0e44c9cfe3 100644 --- a/spec/features/admin/enterprise_roles_spec.rb +++ b/spec/features/admin/enterprise_roles_spec.rb @@ -62,6 +62,20 @@ feature %q{ page.should have_content "That role is already present." end.to change(EnterpriseRole, :count).by(0) end + + scenario "deleting a relationship" do + u = create(:user, email: 'u@example.com') + e = create(:enterprise, name: 'One') + er = create(:enterprise_role, user: u, enterprise: e) + + visit admin_enterprise_roles_path + page.should have_relationship u, e + + first("a.delete-enterprise-role").click + + page.should_not have_relationship u, e + EnterpriseRole.where(id: er.id).should be_empty + end end From 63f9abdf5cb5568fa470a36307853565e96a39d0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 Aug 2014 16:08:50 +1000 Subject: [PATCH 143/205] Add CSS3 animations to enterprise roles admin --- ...enterprise_relationships.css.sass => relationships.css.sass} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/assets/stylesheets/admin/{enterprise_relationships.css.sass => relationships.css.sass} (87%) diff --git a/app/assets/stylesheets/admin/enterprise_relationships.css.sass b/app/assets/stylesheets/admin/relationships.css.sass similarity index 87% rename from app/assets/stylesheets/admin/enterprise_relationships.css.sass rename to app/assets/stylesheets/admin/relationships.css.sass index cffc0b0623..a6467958b7 100644 --- a/app/assets/stylesheets/admin/enterprise_relationships.css.sass +++ b/app/assets/stylesheets/admin/relationships.css.sass @@ -7,7 +7,7 @@ background-color: #fff -table#enterprise-relationships +table#enterprise-relationships, table#enterprise-roles th.actions, td.actions width: 16% .errors From a0a752a3b1935f860bfe3f929269a6bd46c41233 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 Aug 2014 17:34:24 +1000 Subject: [PATCH 144/205] Don't re-create FK --- ...9_add_unique_and_fk_constraints_to_enterprise_roles.rb | 8 -------- ...815053659_add_unique_constraint_to_enterprise_roles.rb | 5 +++++ 2 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 db/migrate/20140815053659_add_unique_and_fk_constraints_to_enterprise_roles.rb create mode 100644 db/migrate/20140815053659_add_unique_constraint_to_enterprise_roles.rb diff --git a/db/migrate/20140815053659_add_unique_and_fk_constraints_to_enterprise_roles.rb b/db/migrate/20140815053659_add_unique_and_fk_constraints_to_enterprise_roles.rb deleted file mode 100644 index 782cad0f20..0000000000 --- a/db/migrate/20140815053659_add_unique_and_fk_constraints_to_enterprise_roles.rb +++ /dev/null @@ -1,8 +0,0 @@ -class AddUniqueAndFkConstraintsToEnterpriseRoles < ActiveRecord::Migration - def change - add_index :enterprise_roles, [:user_id, :enterprise_id], unique: true - - add_foreign_key :enterprise_roles, :spree_users, column: :user_id - add_foreign_key :enterprise_roles, :enterprises, column: :enterprise_id - end -end diff --git a/db/migrate/20140815053659_add_unique_constraint_to_enterprise_roles.rb b/db/migrate/20140815053659_add_unique_constraint_to_enterprise_roles.rb new file mode 100644 index 0000000000..68d7740c64 --- /dev/null +++ b/db/migrate/20140815053659_add_unique_constraint_to_enterprise_roles.rb @@ -0,0 +1,5 @@ +class AddUniqueConstraintToEnterpriseRoles < ActiveRecord::Migration + def change + add_index :enterprise_roles, [:user_id, :enterprise_id], unique: true + end +end From 2660ffd459e0dfaac4850df335d666c458b24f5f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Sat, 16 Aug 2014 16:33:07 +1000 Subject: [PATCH 145/205] Fix duplicate enterprise role --- spec/features/admin/orders_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 6c10c37541..dd733a7009 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -119,7 +119,6 @@ feature %q{ before(:each) do @enterprise_user = create_enterprise_user @enterprise_user.enterprise_roles.build(enterprise: supplier1).save - @enterprise_user.enterprise_roles.build(enterprise: supplier1).save @enterprise_user.enterprise_roles.build(enterprise: coordinator1).save @enterprise_user.enterprise_roles.build(enterprise: distributor1).save From a858c21296e3a0582075bba318e55d84aa066a72 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 11:37:31 +1000 Subject: [PATCH 146/205] Enterprise has a type (full, single, profile), editable by admin --- app/models/enterprise.rb | 9 ++++++--- app/views/admin/enterprises/_form.html.haml | 18 ++++++++++++++++++ .../20140815065014_add_type_to_enterprises.rb | 10 ++++++++++ db/schema.rb | 7 ++++--- spec/features/admin/enterprises_spec.rb | 2 ++ 5 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20140815065014_add_type_to_enterprises.rb diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 65a30fe696..004923097e 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -1,6 +1,9 @@ class Enterprise < ActiveRecord::Base + TYPES = %w(full single profile) ENTERPRISE_SEARCH_RADIUS = 100 + self.inheritance_column = nil + acts_as_gmappable :process_geocoding => false has_and_belongs_to_many :groups, class_name: 'EnterpriseGroup' @@ -40,9 +43,9 @@ class Enterprise < ActiveRecord::Base supports_s3 :promo_image - validates_presence_of :name - validates_presence_of :address - validates_associated :address + validates :name, presence: true + validates :type, presence: true, inclusion: {in: TYPES} + validates :address, presence: true, associated: true before_validation :set_unused_address_fields after_validation :geocode_address diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index e21e653a05..07be41dd6e 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -36,6 +36,24 @@ = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer'   = f.label :is_primary_producer, 'Producer' + .row + .alpha.eleven.columns + .three.columns.alpha + = f.label :type, 'Profile type' + .with-tip{'data-powertip' => "Full - enterprise may have products and relationships.
Single - enterprise may have products but no relationships.
Profile - enterprise has a profile but no products or relationships.
"} + %a What's this? + .two.columns + = f.radio_button :type, "full" +   + = f.label :type, "Full", value: "full" + .two.columns + = f.radio_button :type, "single" +   + = f.label :type, "Single", value: "single" + .four.columns.omega + = f.radio_button :type, "profile" +   + = f.label :type, "Profile", value: "profile" .row .three.columns.alpha %label Visible in search? diff --git a/db/migrate/20140815065014_add_type_to_enterprises.rb b/db/migrate/20140815065014_add_type_to_enterprises.rb new file mode 100644 index 0000000000..0317cac350 --- /dev/null +++ b/db/migrate/20140815065014_add_type_to_enterprises.rb @@ -0,0 +1,10 @@ +class AddTypeToEnterprises < ActiveRecord::Migration + def up + add_column :enterprises, :type, :string, null: false, default: 'profile' + Enterprise.update_all type: 'full' + end + + def down + remove_column :enterprises, :type + end +end diff --git a/db/schema.rb b/db/schema.rb index 0ef402ecf5..2c9c6b24ff 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 => 20140815053659) do +ActiveRecord::Schema.define(:version => 20140815065014) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -241,8 +241,8 @@ ActiveRecord::Schema.define(:version => 20140815053659) do t.integer "address_id" t.string "pickup_times" t.string "next_collection_at" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.text "distributor_info" t.string "logo_file_name" t.string "logo_content_type" @@ -256,6 +256,7 @@ ActiveRecord::Schema.define(:version => 20140815053659) do t.string "facebook" t.string "instagram" t.string "linkedin" + t.string "type", :default => "profile", :null => false end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 6ec19098fe..da32f92681 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -73,6 +73,7 @@ feature %q{ click_link 'New Enterprise' fill_in 'enterprise_name', :with => 'Eaterprises' + choose 'Full' fill_in 'enterprise_description', :with => 'Connecting farmers and eaters' fill_in 'enterprise_long_description', :with => 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.' fill_in 'enterprise_distributor_info', :with => 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.' @@ -123,6 +124,7 @@ feature %q{ all("a", text:'Edit Profile').first.click fill_in 'enterprise_name', :with => 'Eaterprises' + choose 'Single' fill_in 'enterprise_description', :with => 'Connecting farmers and eaters' fill_in 'enterprise_long_description', :with => 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.' From 732a61664f6a17816b3430a1616be6741d6a3cb4 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 13:21:20 +1000 Subject: [PATCH 147/205] Split abilities into enterprises/products/relationships --- app/models/spree/ability_decorator.rb | 150 +++++++++++++++----------- 1 file changed, 88 insertions(+), 62 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index a04d966ee7..366d27d1de 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -2,85 +2,111 @@ class AbilityDecorator include CanCan::Ability def initialize(user) - if user.enterprises.count > 0 + add_enterprise_management_abilities user if can_manage_enterprises? user + add_product_management_abilities user if can_manage_products? user + add_relationship_management_abilities user if can_manage_relationships? user + end - # Spree performs authorize! on (:create, nil) when creating a new order from admin, and also (:search, nil) - # when searching for variants to add to the order - can [:create, :search, :bulk_update], nil - can [:admin, :index], :overview + def can_manage_enterprises?(user) + user.enterprises.present? + end - # Enterprise User can only access products that they are a supplier for - can [:create], Spree::Product - can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], Spree::Product do |product| - user.enterprises.include? product.supplier - end - can [:create], Spree::Variant - can [:admin, :index, :read, :edit, :update, :search, :destroy], Spree::Variant do |variant| - user.enterprises.include? variant.product.supplier - end + def can_manage_products?(user) + can_manage_enterprises? user + # ( user.enterprises.map(&:type) & %w(single full) ).any? + end - can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty - can [:admin, :index, :read, :create, :edit, :update, :destroy], Spree::Image - can [:admin, :index, :read, :search], Spree::Taxon - can [:admin, :index, :read, :create, :edit], Spree::Classification + def can_manage_relationships?(user) + can_manage_enterprises? user + end - # Enterprise User can only access orders that they are a distributor for - can [:index, :create], Spree::Order - can [:read, :update, :bulk_management, :fire, :resend], Spree::Order do |order| - # We allow editing orders with a nil distributor as this state occurs - # during the order creation process from the admin backend - order.distributor.nil? || user.enterprises.include?(order.distributor) - end - can [:admin], Spree::Order if user.admin? || user.enterprises.any?{ |e| e.is_distributor? } - can [:admin, :create], Spree::LineItem - can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Payment - can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Shipment - can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Adjustment - can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization + def add_enterprise_management_abilities(user) + # Spree performs authorize! on (:create, nil) when creating a new order from admin, and also (:search, nil) + # when searching for variants to add to the order + can [:create, :search, :bulk_update], nil - # Enterprise User can only access payment methods for their distributors - can [:index, :create], Spree::PaymentMethod - can [:admin, :read, :update, :fire, :resend, :destroy, :show_provider_preferences], Spree::PaymentMethod do |payment_method| - (user.enterprises & payment_method.distributors).any? - end + can [:admin, :index], :overview - can [:index, :create], Spree::ShippingMethod - can [:admin, :read, :update, :destroy], Spree::ShippingMethod do |shipping_method| - (user.enterprises & shipping_method.distributors).any? - end + # Enterprise User can only access payment methods for their distributors + can [:index, :create], Spree::PaymentMethod + can [:admin, :read, :update, :fire, :resend, :destroy, :show_provider_preferences], Spree::PaymentMethod do |payment_method| + (user.enterprises & payment_method.distributors).any? + end - can [:admin, :index, :create], EnterpriseRelationship - can [:destroy], EnterpriseRelationship do |enterprise_relationship| - user.enterprises.include? enterprise_relationship.parent - end + can [:index, :create], Spree::ShippingMethod + can [:admin, :read, :update, :destroy], Spree::ShippingMethod do |shipping_method| + (user.enterprises & shipping_method.distributors).any? + end - can [:create], OrderCycle - can [:admin, :index, :read, :edit, :update, :bulk_update, :clone], OrderCycle do |order_cycle| - user.enterprises.include? order_cycle.coordinator - end + can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty - can [:index, :create], EnterpriseFee - can [:admin, :read, :edit, :bulk_update, :destroy], EnterpriseFee do |enterprise_fee| - user.enterprises.include? enterprise_fee.enterprise - end + can [:admin, :index, :create], Enterprise + can [:read, :edit, :update, :bulk_update], Enterprise do |enterprise| + user.enterprises.include? enterprise + end + end - can [:admin, :index, :read, :create, :edit, :update], ExchangeVariant - can [:admin, :index, :read, :create, :edit, :update], Exchange - can [:admin, :index, :read, :create, :edit, :update], ExchangeFee - can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty + def add_product_management_abilities(user) + # Enterprise User can only access products that they are a supplier for + can [:create], Spree::Product + can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], Spree::Product do |product| + user.enterprises.include? product.supplier + end - can [:admin, :index, :create], Enterprise - can [:read, :edit, :update, :bulk_update], Enterprise do |enterprise| - user.enterprises.include? enterprise - end + can [:create], Spree::Variant + can [:admin, :index, :read, :edit, :update, :search, :destroy], Spree::Variant do |variant| + user.enterprises.include? variant.product.supplier + end - # Enterprise User can access reports page - can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report + can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty + can [:admin, :index, :read, :create, :edit, :update, :destroy], Spree::Image + + can [:admin, :index, :read, :search], Spree::Taxon + can [:admin, :index, :read, :create, :edit], Spree::Classification + + # Enterprise User can only access orders that they are a distributor for + can [:index, :create], Spree::Order + can [:read, :update, :bulk_management, :fire, :resend], Spree::Order do |order| + # We allow editing orders with a nil distributor as this state occurs + # during the order creation process from the admin backend + order.distributor.nil? || user.enterprises.include?(order.distributor) + end + can [:admin], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor?) + can [:admin, :create], Spree::LineItem + + can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Payment + can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Shipment + can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Adjustment + can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization + + can [:create], OrderCycle + can [:admin, :index, :read, :edit, :update, :bulk_update, :clone], OrderCycle do |order_cycle| + user.enterprises.include? order_cycle.coordinator + end + + can [:index, :create], EnterpriseFee + can [:admin, :read, :edit, :bulk_update, :destroy], EnterpriseFee do |enterprise_fee| + user.enterprises.include? enterprise_fee.enterprise + end + + can [:admin, :index, :read, :create, :edit, :update], ExchangeVariant + can [:admin, :index, :read, :create, :edit, :update], Exchange + can [:admin, :index, :read, :create, :edit, :update], ExchangeFee + + # Reports page + can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report + end + + + def add_relationship_management_abilities(user) + can [:admin, :index, :create], EnterpriseRelationship + can [:destroy], EnterpriseRelationship do |enterprise_relationship| + user.enterprises.include? enterprise_relationship.parent end end end From 0e6cd30e7ec00b2dbf9e5b1a7670abc2d798278a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 13:27:45 +1000 Subject: [PATCH 148/205] Edit for clarity --- spec/features/admin/enterprise_user_spec.rb | 85 +++++++++------------ 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/spec/features/admin/enterprise_user_spec.rb b/spec/features/admin/enterprise_user_spec.rb index bf96f9eda2..3a0ea1cc28 100644 --- a/spec/features/admin/enterprise_user_spec.rb +++ b/spec/features/admin/enterprise_user_spec.rb @@ -7,71 +7,64 @@ feature %q{ include AuthenticationWorkflow include WebHelper - before(:each) do - @new_user = create_enterprise_user - @supplier1 = create(:supplier_enterprise, name: 'Supplier 1') - @supplier2 = create(:supplier_enterprise, name: 'Supplier 2') - @distributor1 = create(:distributor_enterprise, name: 'Distributor 3') - @distributor2 = create(:distributor_enterprise, name: 'Distributor 4') - end + let!(:user) { create_enterprise_user } + let!(:supplier1) { create(:supplier_enterprise, name: 'Supplier 1') } + let!(:supplier2) { create(:supplier_enterprise, name: 'Supplier 2') } + let!(:distributor1) { create(:distributor_enterprise, name: 'Distributor 3') } + let!(:distributor2) { create(:distributor_enterprise, name: 'Distributor 4') } - context "creating an Enterprise User" do - context 'with no enterprises' do - scenario "assigning a user to an Enterprise" do + describe "creating an enterprise user" do + context "with no enterprises managed" do + it "assigns an enterprise to a user" do login_to_admin_section click_link 'Users' - click_link @new_user.email + click_link user.email click_link 'Edit' - check @supplier2.name + check supplier2.name click_button 'Update' - @new_user.enterprises.count.should == 1 - @new_user.enterprises.first.name.should == @supplier2.name + user.enterprises.count.should == 1 + user.enterprises.first.name.should == supplier2.name end - end - context 'with existing enterprises' do - - before(:each) do - @new_user.enterprise_roles.build(enterprise: @supplier1).save - @new_user.enterprise_roles.build(enterprise: @distributor1).save + context "with existing enterprises managed" do + before do + user.enterprise_roles.create!(enterprise: supplier1) + user.enterprise_roles.create!(enterprise: distributor1) end - scenario "removing and add enterprises for a user" do + it "can remove and add enterprise management for a user" do login_to_admin_section click_link 'Users' - click_link @new_user.email + click_link user.email click_link 'Edit' - uncheck @distributor1.name # remove - check @distributor2.name # add + uncheck distributor1.name # remove + check distributor2.name # add click_button 'Update' - @new_user.enterprises.count.should == 2 - @new_user.enterprises.should include(@supplier1) - @new_user.enterprises.should include(@distributor2) + user.enterprises.count.should == 2 + user.enterprises.should include supplier1 + user.enterprises.should include distributor2 end - end - end - context "Product management" do - - context 'products I supply' do - before(:each) do - @new_user.enterprise_roles.build(enterprise: @supplier1).save - product1 = create(:product, name: 'Green eggs', supplier: @supplier1) - product2 = create(:product, name: 'Ham', supplier: @supplier2) - login_to_admin_as @new_user + describe "product management" do + describe "managing supplied products" do + before do + user.enterprise_roles.create!(enterprise: supplier1) + product1 = create(:product, name: 'Green eggs', supplier: supplier1) + product2 = create(:product, name: 'Ham', supplier: supplier2) + login_to_admin_as user end - scenario "manage products that I supply" do - visit '/admin/products' + it "can manage products that I supply" do + visit spree.admin_products_path within '#listing_products' do page.should have_content 'Green eggs' @@ -79,23 +72,21 @@ feature %q{ end end end - end - context "System management lockdown" do - - before(:each) do - @new_user.enterprise_roles.build(enterprise: @supplier1).save - login_to_admin_as @new_user + describe "system management lockdown" do + before do + user.enterprise_roles.create!(enterprise: supplier1) + login_to_admin_as user end scenario "should not be able to see system configuration" do - visit '/admin/general_settings/edit' + visit spree.edit_admin_general_settings_path page.should have_content 'Unauthorized' end scenario "should not be able to see user management" do - visit '/admin/users' + visit spree.admin_users_path page.should have_content 'Unauthorized' end end From abe592c9a3ea443396c5aad5f3242cd469d648f8 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 14:22:19 +1000 Subject: [PATCH 149/205] Allow Reports admin tab to be enabled/disabled through cancan :report resource --- .../admin/navigation_helper_decorator.rb | 16 ++++++++++++++ spec/helpers/navigation_helper_spec.rb | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 app/helpers/spree/admin/navigation_helper_decorator.rb create mode 100644 spec/helpers/navigation_helper_spec.rb diff --git a/app/helpers/spree/admin/navigation_helper_decorator.rb b/app/helpers/spree/admin/navigation_helper_decorator.rb new file mode 100644 index 0000000000..024f467154 --- /dev/null +++ b/app/helpers/spree/admin/navigation_helper_decorator.rb @@ -0,0 +1,16 @@ +module Spree + module Admin + module NavigationHelper + # Make it so that the Reports admin tab can be enabled/disabled through the cancan + # :report resource, since it does not have a corresponding resource class (unlike + # eg. Spree::Product). + def klass_for_with_sym_fallback(name) + klass = klass_for_without_sym_fallback(name) + klass ||= name.singularize.to_sym + klass = :overview if klass == :dashboard + klass + end + alias_method_chain :klass_for, :sym_fallback + end + end +end diff --git a/spec/helpers/navigation_helper_spec.rb b/spec/helpers/navigation_helper_spec.rb new file mode 100644 index 0000000000..41decd6e5a --- /dev/null +++ b/spec/helpers/navigation_helper_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +module Spree + module Admin + describe NavigationHelper do + describe "klass_for" do + it "returns the class when present" do + helper.klass_for('products').should == Spree::Product + end + + it "returns a symbol when there's no available class" do + helper.klass_for('reports').should == :report + end + + it "returns :overview for the dashboard" do + helper.klass_for('dashboard').should == :overview + end + end + end + end +end From 50e4c5fac9ac6eb6ccae9a8a77d1f9e554550104 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 14:26:45 +1000 Subject: [PATCH 150/205] Users with only a profile-level enterprise see only menu items for enterprise management --- app/models/spree/ability_decorator.rb | 3 +- spec/factories.rb | 1 + spec/features/admin/enterprise_user_spec.rb | 21 +++++++++++ spec/models/spree/ability_spec.rb | 40 +++++++++++++++++++++ spec/support/request/admin_helper.rb | 5 +++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 spec/support/request/admin_helper.rb diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 366d27d1de..b17fe3ce40 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -14,8 +14,7 @@ class AbilityDecorator def can_manage_products?(user) - can_manage_enterprises? user - # ( user.enterprises.map(&:type) & %w(single full) ).any? + ( user.enterprises.map(&:type) & %w(single full) ).any? end diff --git a/spec/factories.rb b/spec/factories.rb index 7102229cff..849d38abf3 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -83,6 +83,7 @@ FactoryGirl.define do factory :enterprise, :class => Enterprise do sequence(:name) { |n| "Enterprise #{n}" } + type 'full' description 'enterprise' long_description '

Hello, world!

This is a paragraph.

' email 'enterprise@example.com' diff --git a/spec/features/admin/enterprise_user_spec.rb b/spec/features/admin/enterprise_user_spec.rb index 3a0ea1cc28..a75eea6b8b 100644 --- a/spec/features/admin/enterprise_user_spec.rb +++ b/spec/features/admin/enterprise_user_spec.rb @@ -6,10 +6,12 @@ feature %q{ } do include AuthenticationWorkflow include WebHelper + include AdminHelper let!(:user) { create_enterprise_user } let!(:supplier1) { create(:supplier_enterprise, name: 'Supplier 1') } let!(:supplier2) { create(:supplier_enterprise, name: 'Supplier 2') } + let(:supplier_profile) { create(:supplier_enterprise, name: 'Supplier profile', type: 'profile') } let!(:distributor1) { create(:distributor_enterprise, name: 'Distributor 3') } let!(:distributor2) { create(:distributor_enterprise, name: 'Distributor 4') } @@ -74,6 +76,25 @@ feature %q{ end end + describe "with only a profile-level enterprise" do + before do + user.enterprise_roles.create! enterprise: supplier_profile + login_to_admin_as user + end + + it "shows me only menu items for enterprise management" do + page.should have_admin_menu_item 'Dashboard' + page.should have_admin_menu_item 'Enterprises' + + ['Orders', 'Products', 'Reports', 'Configuration', 'Promotions', 'Users', 'Order Cycles'].each do |menu_item_name| + page.should_not have_admin_menu_item menu_item_name + end + end + + it "shows me a cut-down dashboard" + it "shows me only profile options on the enterprises page" + end + describe "system management lockdown" do before do user.enterprise_roles.create!(enterprise: supplier1) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 9705fee70e..a2d1fb5bdf 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -6,6 +6,46 @@ module Spree describe User do + describe "broad permissions" do + subject { AbilityDecorator.new(user) } + let(:user) { create(:user) } + let(:enterprise_full) { create(:enterprise, type: 'full') } + let(:enterprise_single) { create(:enterprise, type: 'single') } + let(:enterprise_profile) { create(:enterprise, type: 'profile') } + + describe "managing enterprises" do + it "can manage enterprises when the user has at least one enterprise assigned" do + user.enterprise_roles.create! enterprise: enterprise_full + subject.can_manage_enterprises?(user).should be_true + end + + it "can't otherwise" do + subject.can_manage_enterprises?(user).should be_false + end + end + + describe "managing products" do + it "can when a user manages a 'full' type enterprise" do + user.enterprise_roles.create! enterprise: enterprise_full + subject.can_manage_products?(user).should be_true + end + + it "can when a user manages a 'single' type enterprise" do + user.enterprise_roles.create! enterprise: enterprise_single + subject.can_manage_products?(user).should be_true + end + + it "can't when a user manages a 'profile' type enterprise" do + user.enterprise_roles.create! enterprise: enterprise_profile + subject.can_manage_products?(user).should be_false + end + + it "can't when the user manages no enterprises" do + subject.can_manage_products?(user).should be_false + end + end + end + describe 'Roles' do # create enterprises diff --git a/spec/support/request/admin_helper.rb b/spec/support/request/admin_helper.rb new file mode 100644 index 0000000000..ff0c50b6f5 --- /dev/null +++ b/spec/support/request/admin_helper.rb @@ -0,0 +1,5 @@ +module AdminHelper + def have_admin_menu_item(menu_item_name) + have_selector "ul[data-hook='admin_tabs'] li", text: menu_item_name + end +end From e1a1b74f038f88349722bff687e8d2f12bbc8b34 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 14:49:16 +1000 Subject: [PATCH 151/205] Extract dashboard enterprises table markup into partials --- .../admin/overview/_enterprises.html.haml | 92 ++----------------- .../overview/_enterprises_footer.html.haml | 3 + .../overview/_enterprises_header.html.haml | 7 ++ .../overview/_enterprises_hubs_tab.html.haml | 35 +++++++ .../overview/_enterprises_none.html.haml | 7 ++ .../_enterprises_producers_tab.html.haml | 28 ++++++ .../overview/_enterprises_tabs.html.haml | 3 + 7 files changed, 91 insertions(+), 84 deletions(-) create mode 100644 app/views/spree/admin/overview/_enterprises_footer.html.haml create mode 100644 app/views/spree/admin/overview/_enterprises_header.html.haml create mode 100644 app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml create mode 100644 app/views/spree/admin/overview/_enterprises_none.html.haml create mode 100644 app/views/spree/admin/overview/_enterprises_producers_tab.html.haml create mode 100644 app/views/spree/admin/overview/_enterprises_tabs.html.haml diff --git a/app/views/spree/admin/overview/_enterprises.html.haml b/app/views/spree/admin/overview/_enterprises.html.haml index 85dc93c2ee..757755c718 100644 --- a/app/views/spree/admin/overview/_enterprises.html.haml +++ b/app/views/spree/admin/overview/_enterprises.html.haml @@ -1,87 +1,11 @@ %div.dashboard_item.sixteen.columns.alpha#enterprises{ 'ng-app' => 'ofn.admin', 'ng-controller' => "enterprisesDashboardCtrl" } - %div.header.sixteen.columns.alpha{ :class => "#{@enterprises.count > 0 ? "" : "red"}"} - %h3.thirteen.columns.alpha My Enterprises - - if @enterprises.any? - %a.three.columns.omega.icon-plus.button.blue.white-bottom{ href: "#{main_app.new_admin_enterprise_path}" } - CREATE NEW - - else - %a.with-tip{ title: "Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network." } What's this? - - if @enterprises.any? - %div.sixteen.columns.alpha.tabs - %div.dashboard_tab.eight.columns.alpha.blue{ ng: { class: "{selected: activeTab == 'hubs'}", click: "activeTab = 'hubs'" } } HUBS - %div.dashboard_tab.eight.columns.omega.blue{ ng: { class: "{selected: activeTab == 'producers'}", click: "activeTab = 'producers'" } } PRODUCERS + = render 'enterprises_header' + - if @enterprises.empty? - %div.sixteen.columns.alpha.list-item.red - %span.text.fifteen.columns.alpha You don't have any enterprises yet. - %span.one.columns.omega - %span.icon-remove-sign - %a.sixteen.columns.alpha.button.bottom.red{ href: "#{main_app.new_admin_enterprise_path}" } - CREATE A NEW ENTERPRISE - %span.icon-arrow-right + = render 'enterprises_none' + - else - %div.hubs_tab{ ng: { show: "activeTab == 'hubs'"} } - %div.sixteen.columns.alpha.list-title - %span.five.columns.alpha Name - %span.centered.three.columns Payment Methods - %span.centered.three.columns Shipping Methods - %span.centered.three.columns Enterprise Fees - %div.sixteen.columns.alpha.list - - @enterprises.is_distributor.each do |enterprise| - %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } - %span.five.columns.alpha - = enterprise.name - %span.symbol.three.columns.centered - - payment_method_count = enterprise.payment_methods.count - - if payment_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no Payment Methods" } - - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require Payment Methods." } - - else - %span.icon-ok-sign.with-tip{ title: "#{payment_method_count} Payment Method#{payment_method_count > 1 ? "s" : ""}" } - %span.symbol.three.columns.centered - - shipping_method_count = enterprise.shipping_methods.count - - if shipping_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no Shipping Methods" } - - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require Shipping Methods." } - -else - %span.icon-ok-sign.with-tip{ title: "#{shipping_method_count} Shipping Method#{shipping_method_count > 1 ? "s" : ""}" } - %span.symbol.three.columns.centered - - fee_count = enterprise.enterprise_fees.count - - if fee_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{fee_count} Fee#{fee_count > 1 ? "s" : ""}" } - - else - %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no Enterprise Fees" } - %span.two.columns.omega.right - %span.icon-arrow-right - %div.producers_tab{ ng: { show: "activeTab == 'producers'"} } - %div.list-title.sixteen.columns.alpha - %span.five.columns.alpha Name - %span.centered.three.columns Total Products - %span.centered.three.columns Active Products - %span.centered.three.columns Products in OCs - %div.sixteen.columns.alpha.list - - @enterprises.is_primary_producer.each do |enterprise| - %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } - %span.five.columns.alpha - = enterprise.name - %span.symbol.three.columns.centered - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.count > 0 ? "green" : "red" }" } - = enterprise.supplied_products.not_deleted.count - %span.one.column.omega   - %span.symbol.three.columns.centered - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.count > 0 ? "green" : "red" }" } - = enterprise.supplied_and_active_products_on_hand.count - %span.one.column.omega   - %span.symbol.three.columns.centered - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.count > 0 ? "green" : "orange" }" } - = enterprise.active_products_in_order_cycles.count - %span.one.column.omega   - %span.two.columns.omega.right - %span.icon-arrow-right - %a.sixteen.columns.alpha.button.bottom.blue{ href: "#{main_app.admin_enterprises_path}" } - MANAGE MY ENTERPRISES - %span.icon-arrow-right \ No newline at end of file + = render 'enterprises_tabs' + = render 'enterprises_hubs_tab' + = render 'enterprises_producers_tab' + = render 'enterprises_footer' diff --git a/app/views/spree/admin/overview/_enterprises_footer.html.haml b/app/views/spree/admin/overview/_enterprises_footer.html.haml new file mode 100644 index 0000000000..c61add38f5 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_footer.html.haml @@ -0,0 +1,3 @@ +%a.sixteen.columns.alpha.button.bottom.blue{ href: "#{main_app.admin_enterprises_path}" } + MANAGE MY ENTERPRISES + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_header.html.haml b/app/views/spree/admin/overview/_enterprises_header.html.haml new file mode 100644 index 0000000000..d53828ca65 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_header.html.haml @@ -0,0 +1,7 @@ +%div.header.sixteen.columns.alpha{ :class => "#{@enterprises.count > 0 ? "" : "red"}"} + %h3.thirteen.columns.alpha My Enterprises + - if @enterprises.any? + %a.three.columns.omega.icon-plus.button.blue.white-bottom{ href: "#{main_app.new_admin_enterprise_path}" } + CREATE NEW + - else + %a.with-tip{ title: "Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network." } What's this? diff --git a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml new file mode 100644 index 0000000000..97782817fe --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml @@ -0,0 +1,35 @@ +%div.hubs_tab{ ng: { show: "activeTab == 'hubs'"} } + %div.sixteen.columns.alpha.list-title + %span.five.columns.alpha Name + %span.centered.three.columns Payment Methods + %span.centered.three.columns Shipping Methods + %span.centered.three.columns Enterprise Fees + %div.sixteen.columns.alpha.list + - @enterprises.is_distributor.each do |enterprise| + %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } + %span.five.columns.alpha + = enterprise.name + %span.symbol.three.columns.centered + - payment_method_count = enterprise.payment_methods.count + - if payment_method_count < 1 && enterprise.is_distributor + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no Payment Methods" } + - elsif enterprise.is_primary_producer + %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require Payment Methods." } + - else + %span.icon-ok-sign.with-tip{ title: "#{payment_method_count} Payment Method#{payment_method_count > 1 ? "s" : ""}" } + %span.symbol.three.columns.centered + - shipping_method_count = enterprise.shipping_methods.count + - if shipping_method_count < 1 && enterprise.is_distributor + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no Shipping Methods" } + - elsif enterprise.is_primary_producer + %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require Shipping Methods." } + -else + %span.icon-ok-sign.with-tip{ title: "#{shipping_method_count} Shipping Method#{shipping_method_count > 1 ? "s" : ""}" } + %span.symbol.three.columns.centered + - fee_count = enterprise.enterprise_fees.count + - if fee_count > 0 + %span.icon-ok-sign.with-tip{ title: "#{fee_count} Fee#{fee_count > 1 ? "s" : ""}" } + - else + %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no Enterprise Fees" } + %span.two.columns.omega.right + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_none.html.haml b/app/views/spree/admin/overview/_enterprises_none.html.haml new file mode 100644 index 0000000000..c1428a6863 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_none.html.haml @@ -0,0 +1,7 @@ +%div.sixteen.columns.alpha.list-item.red + %span.text.fifteen.columns.alpha You don't have any enterprises yet. + %span.one.columns.omega + %span.icon-remove-sign +%a.sixteen.columns.alpha.button.bottom.red{ href: "#{main_app.new_admin_enterprise_path}" } + CREATE A NEW ENTERPRISE + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml new file mode 100644 index 0000000000..bece8d91e9 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml @@ -0,0 +1,28 @@ +%div.producers_tab{ ng: { show: "activeTab == 'producers'"} } + %div.list-title.sixteen.columns.alpha + %span.five.columns.alpha Name + %span.centered.three.columns Total Products + %span.centered.three.columns Active Products + %span.centered.three.columns Products in OCs + %div.sixteen.columns.alpha.list + - @enterprises.is_primary_producer.each do |enterprise| + %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } + %span.five.columns.alpha + = enterprise.name + %span.symbol.three.columns.centered + %span.one.column.alpha   + %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.count > 0 ? "green" : "red" }" } + = enterprise.supplied_products.not_deleted.count + %span.one.column.omega   + %span.symbol.three.columns.centered + %span.one.column.alpha   + %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.count > 0 ? "green" : "red" }" } + = enterprise.supplied_and_active_products_on_hand.count + %span.one.column.omega   + %span.symbol.three.columns.centered + %span.one.column.alpha   + %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.count > 0 ? "green" : "orange" }" } + = enterprise.active_products_in_order_cycles.count + %span.one.column.omega   + %span.two.columns.omega.right + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_tabs.html.haml b/app/views/spree/admin/overview/_enterprises_tabs.html.haml new file mode 100644 index 0000000000..f6e5413786 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_tabs.html.haml @@ -0,0 +1,3 @@ +%div.sixteen.columns.alpha.tabs + %div.dashboard_tab.eight.columns.alpha.blue{ ng: { class: "{selected: activeTab == 'hubs'}", click: "activeTab = 'hubs'" } } HUBS + %div.dashboard_tab.eight.columns.omega.blue{ ng: { class: "{selected: activeTab == 'producers'}", click: "activeTab = 'producers'" } } PRODUCERS From 9451f3659c61cf0632ef9ce7066f4740cc1a0115 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 15:41:01 +1000 Subject: [PATCH 152/205] Use pluralize helper, sentence case --- .../overview/_enterprises_hubs_tab.html.haml | 16 ++++++++-------- .../_enterprises_producers_tab.html.haml | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml index 97782817fe..4695f65503 100644 --- a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml @@ -12,24 +12,24 @@ %span.symbol.three.columns.centered - payment_method_count = enterprise.payment_methods.count - if payment_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no Payment Methods" } + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no payment methods" } - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require Payment Methods." } + %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require payment methods." } - else - %span.icon-ok-sign.with-tip{ title: "#{payment_method_count} Payment Method#{payment_method_count > 1 ? "s" : ""}" } + %span.icon-ok-sign.with-tip{ title: "#{pluralize payment_method_count, 'payment method'}" } %span.symbol.three.columns.centered - shipping_method_count = enterprise.shipping_methods.count - if shipping_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no Shipping Methods" } + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no shipping methods" } - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require Shipping Methods." } + %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require shipping methods." } -else - %span.icon-ok-sign.with-tip{ title: "#{shipping_method_count} Shipping Method#{shipping_method_count > 1 ? "s" : ""}" } + %span.icon-ok-sign.with-tip{ title: "#{pluralize shipping_method_count, 'shipping method'}" } %span.symbol.three.columns.centered - fee_count = enterprise.enterprise_fees.count - if fee_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{fee_count} Fee#{fee_count > 1 ? "s" : ""}" } + %span.icon-ok-sign.with-tip{ title: "#{pluralize fee_count, 'fee'}" } - else - %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no Enterprise Fees" } + %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no enterprise fees" } %span.two.columns.omega.right %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml index bece8d91e9..4c5cb018e5 100644 --- a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml @@ -11,17 +11,17 @@ = enterprise.name %span.symbol.three.columns.centered %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.count > 0 ? "green" : "red" }" } + %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.any? ? "green" : "red" }" } = enterprise.supplied_products.not_deleted.count %span.one.column.omega   %span.symbol.three.columns.centered %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.count > 0 ? "green" : "red" }" } + %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.any? ? "green" : "red" }" } = enterprise.supplied_and_active_products_on_hand.count %span.one.column.omega   %span.symbol.three.columns.centered %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.count > 0 ? "green" : "orange" }" } + %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.any? ? "green" : "orange" }" } = enterprise.active_products_in_order_cycles.count %span.one.column.omega   %span.two.columns.omega.right From 5dc7d1ee11774d4ef686dfe2ce52d2e1a443a3e6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 16:02:11 +1000 Subject: [PATCH 153/205] For users without product management perms, show a cut-down admin dashboard --- app/models/spree/ability_decorator.rb | 22 ++++---- .../overview/_enterprises_hubs_tab.html.haml | 50 +++++++++++-------- .../_enterprises_producers_tab.html.haml | 39 +++++++++------ .../spree/admin/overview/index.html.haml | 10 ++-- spec/features/admin/enterprise_user_spec.rb | 31 +++++++++++- 5 files changed, 99 insertions(+), 53 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index b17fe3ce40..b29aa6d5b3 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -30,17 +30,6 @@ class AbilityDecorator can [:admin, :index], :overview - # Enterprise User can only access payment methods for their distributors - can [:index, :create], Spree::PaymentMethod - can [:admin, :read, :update, :fire, :resend, :destroy, :show_provider_preferences], Spree::PaymentMethod do |payment_method| - (user.enterprises & payment_method.distributors).any? - end - - can [:index, :create], Spree::ShippingMethod - can [:admin, :read, :update, :destroy], Spree::ShippingMethod do |shipping_method| - (user.enterprises & shipping_method.distributors).any? - end - can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty can [:admin, :index, :create], Enterprise @@ -97,6 +86,17 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update], Exchange can [:admin, :index, :read, :create, :edit, :update], ExchangeFee + # Enterprise user can only access payment and shipping methods for their distributors + can [:index, :create], Spree::PaymentMethod + can [:admin, :read, :update, :fire, :resend, :destroy, :show_provider_preferences], Spree::PaymentMethod do |payment_method| + (user.enterprises & payment_method.distributors).any? + end + + can [:index, :create], Spree::ShippingMethod + can [:admin, :read, :update, :destroy], Spree::ShippingMethod do |shipping_method| + (user.enterprises & shipping_method.distributors).any? + end + # Reports page can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report end diff --git a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml index 4695f65503..482b03d5e3 100644 --- a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml @@ -1,35 +1,41 @@ %div.hubs_tab{ ng: { show: "activeTab == 'hubs'"} } %div.sixteen.columns.alpha.list-title %span.five.columns.alpha Name - %span.centered.three.columns Payment Methods - %span.centered.three.columns Shipping Methods - %span.centered.three.columns Enterprise Fees + - if can? :admin, Spree::PaymentMethod + %span.centered.three.columns Payment Methods + - if can? :admin, Spree::ShippingMethod + %span.centered.three.columns Shipping Methods + - if can? :admin, EnterpriseFee + %span.centered.three.columns Enterprise Fees %div.sixteen.columns.alpha.list - @enterprises.is_distributor.each do |enterprise| %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } %span.five.columns.alpha = enterprise.name %span.symbol.three.columns.centered - - payment_method_count = enterprise.payment_methods.count - - if payment_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no payment methods" } - - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require payment methods." } - - else - %span.icon-ok-sign.with-tip{ title: "#{pluralize payment_method_count, 'payment method'}" } + - if can? :admin, Spree::PaymentMethod + - payment_method_count = enterprise.payment_methods.count + - if payment_method_count < 1 && enterprise.is_distributor + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no payment methods" } + - elsif enterprise.is_primary_producer + %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require payment methods." } + - else + %span.icon-ok-sign.with-tip{ title: "#{pluralize payment_method_count, 'payment method'}" } %span.symbol.three.columns.centered - - shipping_method_count = enterprise.shipping_methods.count - - if shipping_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no shipping methods" } - - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require shipping methods." } - -else - %span.icon-ok-sign.with-tip{ title: "#{pluralize shipping_method_count, 'shipping method'}" } + - if can? :admin, Spree::ShippingMethod + - shipping_method_count = enterprise.shipping_methods.count + - if shipping_method_count < 1 && enterprise.is_distributor + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no shipping methods" } + - elsif enterprise.is_primary_producer + %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require shipping methods." } + - else + %span.icon-ok-sign.with-tip{ title: "#{pluralize shipping_method_count, 'shipping method'}" } %span.symbol.three.columns.centered - - fee_count = enterprise.enterprise_fees.count - - if fee_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{pluralize fee_count, 'fee'}" } - - else - %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no enterprise fees" } + - if can? :admin, EnterpriseFee + - fee_count = enterprise.enterprise_fees.count + - if fee_count > 0 + %span.icon-ok-sign.with-tip{ title: "#{pluralize fee_count, 'fee'}" } + - else + %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no enterprise fees" } %span.two.columns.omega.right %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml index 4c5cb018e5..363733710a 100644 --- a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml @@ -1,28 +1,37 @@ %div.producers_tab{ ng: { show: "activeTab == 'producers'"} } %div.list-title.sixteen.columns.alpha %span.five.columns.alpha Name - %span.centered.three.columns Total Products - %span.centered.three.columns Active Products - %span.centered.three.columns Products in OCs + - if can? :admin, Spree::Product + %span.centered.three.columns Total Products + %span.centered.three.columns Active Products + - if can? :admin, OrderCycle + %span.centered.three.columns Products in OCs %div.sixteen.columns.alpha.list - @enterprises.is_primary_producer.each do |enterprise| %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } + %span.five.columns.alpha = enterprise.name + %span.symbol.three.columns.centered - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.any? ? "green" : "red" }" } - = enterprise.supplied_products.not_deleted.count - %span.one.column.omega   + - if can? :admin, Spree::Product + %span.one.column.alpha   + %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.any? ? "green" : "red" }" } + = enterprise.supplied_products.not_deleted.count + %span.one.column.omega   %span.symbol.three.columns.centered - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.any? ? "green" : "red" }" } - = enterprise.supplied_and_active_products_on_hand.count - %span.one.column.omega   + - if can? :admin, Spree::Product + %span.one.column.alpha   + %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.any? ? "green" : "red" }" } + = enterprise.supplied_and_active_products_on_hand.count + %span.one.column.omega   + %span.symbol.three.columns.centered - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.any? ? "green" : "orange" }" } - = enterprise.active_products_in_order_cycles.count - %span.one.column.omega   + - if can? :admin, OrderCycle + %span.one.column.alpha   + %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.any? ? "green" : "orange" }" } + = enterprise.active_products_in_order_cycles.count + %span.one.column.omega   + %span.two.columns.omega.right %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/index.html.haml b/app/views/spree/admin/overview/index.html.haml index 9fe44ebcca..a46a31b404 100644 --- a/app/views/spree/admin/overview/index.html.haml +++ b/app/views/spree/admin/overview/index.html.haml @@ -5,11 +5,13 @@ = render partial: "spree/admin/overview/enterprises" - else - = render partial: "spree/admin/overview/products" + - if can? :admin, Spree::Product + = render partial: "spree/admin/overview/products" - %div.two.columns -   + %div.two.columns +   - = render partial: "spree/admin/overview/order_cycles" + - if can? :admin, OrderCycle + = render partial: "spree/admin/overview/order_cycles" = render partial: "spree/admin/overview/enterprises" diff --git a/spec/features/admin/enterprise_user_spec.rb b/spec/features/admin/enterprise_user_spec.rb index a75eea6b8b..399e29d794 100644 --- a/spec/features/admin/enterprise_user_spec.rb +++ b/spec/features/admin/enterprise_user_spec.rb @@ -14,6 +14,7 @@ feature %q{ let(:supplier_profile) { create(:supplier_enterprise, name: 'Supplier profile', type: 'profile') } let!(:distributor1) { create(:distributor_enterprise, name: 'Distributor 3') } let!(:distributor2) { create(:distributor_enterprise, name: 'Distributor 4') } + let(:distributor_profile) { create(:distributor_enterprise, name: 'Distributor profile', type: 'profile') } describe "creating an enterprise user" do context "with no enterprises managed" do @@ -79,6 +80,7 @@ feature %q{ describe "with only a profile-level enterprise" do before do user.enterprise_roles.create! enterprise: supplier_profile + user.enterprise_roles.create! enterprise: distributor_profile login_to_admin_as user end @@ -91,7 +93,34 @@ feature %q{ end end - it "shows me a cut-down dashboard" + describe "dashboard" do + it "shows me enterprise management controls" do + within('#enterprises') do + page.should have_selector 'h3', text: 'My Enterprises' + page.should have_link 'CREATE NEW' + page.should have_link supplier_profile.name + page.should have_link 'MANAGE MY ENTERPRISES' + end + end + + it "does not show me product management controls" do + page.should_not have_selector '#products' + page.should_not have_selector '#order_cycles' + end + + it "does not show me enterprise product info, payment methods, shipping methods or enterprise fees" do + # Producer product info + page.should_not have_selector '.producers_tab span', text: 'Total Products' + page.should_not have_selector '.producers_tab span', text: 'Active Products' + page.should_not have_selector '.producers_tab span', text: 'Products in OCs' + + # Payment methods, shipping methods, enterprise fees + page.should_not have_selector '.hubs_tab span', text: 'Payment Methods' + page.should_not have_selector '.hubs_tab span', text: 'Shipping Methods' + page.should_not have_selector '.hubs_tab span', text: 'Enterprise Fees' + end + end + it "shows me only profile options on the enterprises page" end From 2706c0e47e975df677e52a9a3e43413ff90637c7 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 16:11:20 +1000 Subject: [PATCH 154/205] Extract enterprise actions to partial --- .../admin/enterprises/_actions.html.haml | 23 ++++++++++++++++++ app/views/admin/enterprises/index.html.haml | 24 +------------------ 2 files changed, 24 insertions(+), 23 deletions(-) create mode 100644 app/views/admin/enterprises/_actions.html.haml diff --git a/app/views/admin/enterprises/_actions.html.haml b/app/views/admin/enterprises/_actions.html.haml new file mode 100644 index 0000000000..b7abf14a9b --- /dev/null +++ b/app/views/admin/enterprises/_actions.html.haml @@ -0,0 +1,23 @@ += link_to_with_icon('icon-edit', 'Edit Profile', main_app.edit_admin_enterprise_path(enterprise), class: 'edit') +%br/ += link_to_delete_enterprise enterprise +%br/ +- if enterprise.is_primary_producer + = link_to_with_icon 'icon-dashboard', 'Properties', main_app.admin_enterprise_producer_properties_path(enterprise_id: enterprise.id) + (#{enterprise.producer_properties.count}) + %br/ +- if enterprise.is_distributor + = link_to_with_icon 'icon-chevron-right', 'Payment Methods', spree.admin_payment_methods_path(enterprise_id: enterprise.id) + (#{enterprise.payment_methods.count}) + - if enterprise.payment_methods.count == 0 + %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} + %br/ + = link_to_with_icon 'icon-plane', 'Shipping Methods', spree.admin_shipping_methods_path(enterprise_id: enterprise.id) + (#{enterprise.shipping_methods.count}) + - if enterprise.shipping_methods.count == 0 + %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} + %br/ += link_to_with_icon 'icon-money', 'Enterprise Fees', main_app.admin_enterprise_fees_path(enterprise_id: enterprise.id) +(#{enterprise.enterprise_fees.count}) +- if enterprise.enterprise_fees.count == 0 + %span.icon-warning-sign.with-tip{"data-powertip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} diff --git a/app/views/admin/enterprises/index.html.haml b/app/views/admin/enterprises/index.html.haml index 42f12e8632..7ab6ea4d0e 100644 --- a/app/views/admin/enterprises/index.html.haml +++ b/app/views/admin/enterprises/index.html.haml @@ -39,29 +39,7 @@ %td= enterprise_form.check_box :visible %td= enterprise.description %td{"data-hook" => "admin_users_index_row_actions"} - = link_to_with_icon('icon-edit', 'Edit Profile', main_app.edit_admin_enterprise_path(enterprise), class: 'edit') - %br/ - = link_to_delete_enterprise enterprise - %br/ - - if enterprise.is_primary_producer - = link_to_with_icon 'icon-dashboard', 'Properties', main_app.admin_enterprise_producer_properties_path(enterprise_id: enterprise.id) - (#{enterprise.producer_properties.count}) - %br/ - - if enterprise.is_distributor - = link_to_with_icon 'icon-chevron-right', 'Payment Methods', spree.admin_payment_methods_path(enterprise_id: enterprise.id) - (#{enterprise.payment_methods.count}) - - if enterprise.payment_methods.count == 0 - %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} - %br/ - = link_to_with_icon 'icon-plane', 'Shipping Methods', spree.admin_shipping_methods_path(enterprise_id: enterprise.id) - (#{enterprise.shipping_methods.count}) - - if enterprise.shipping_methods.count == 0 - %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} - %br/ - = link_to_with_icon 'icon-money', 'Enterprise Fees', main_app.admin_enterprise_fees_path(enterprise_id: enterprise.id) - (#{enterprise.enterprise_fees.count}) - - if enterprise.enterprise_fees.count == 0 - %span.icon-warning-sign.with-tip{"data-powertip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} + = render 'actions', enterprise: enterprise - if @enterprises.empty? %tr %td{colspan: "4"}= t(:none) From 79387d3d82650c700277df688ede5392cb08563d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 16:15:17 +1000 Subject: [PATCH 155/205] On admin enterprises listing page, do not show links to pages without perms --- .../admin/enterprises/_actions.html.haml | 36 +++++++++++-------- spec/features/admin/enterprise_user_spec.rb | 16 ++++++++- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/app/views/admin/enterprises/_actions.html.haml b/app/views/admin/enterprises/_actions.html.haml index b7abf14a9b..69dd6569a8 100644 --- a/app/views/admin/enterprises/_actions.html.haml +++ b/app/views/admin/enterprises/_actions.html.haml @@ -1,23 +1,31 @@ = link_to_with_icon('icon-edit', 'Edit Profile', main_app.edit_admin_enterprise_path(enterprise), class: 'edit') %br/ + = link_to_delete_enterprise enterprise %br/ + - if enterprise.is_primary_producer = link_to_with_icon 'icon-dashboard', 'Properties', main_app.admin_enterprise_producer_properties_path(enterprise_id: enterprise.id) (#{enterprise.producer_properties.count}) %br/ + - if enterprise.is_distributor - = link_to_with_icon 'icon-chevron-right', 'Payment Methods', spree.admin_payment_methods_path(enterprise_id: enterprise.id) - (#{enterprise.payment_methods.count}) - - if enterprise.payment_methods.count == 0 - %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} - %br/ - = link_to_with_icon 'icon-plane', 'Shipping Methods', spree.admin_shipping_methods_path(enterprise_id: enterprise.id) - (#{enterprise.shipping_methods.count}) - - if enterprise.shipping_methods.count == 0 - %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} - %br/ -= link_to_with_icon 'icon-money', 'Enterprise Fees', main_app.admin_enterprise_fees_path(enterprise_id: enterprise.id) -(#{enterprise.enterprise_fees.count}) -- if enterprise.enterprise_fees.count == 0 - %span.icon-warning-sign.with-tip{"data-powertip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} + - if can? :admin, Spree::PaymentMethod + = link_to_with_icon 'icon-chevron-right', 'Payment Methods', spree.admin_payment_methods_path(enterprise_id: enterprise.id) + (#{enterprise.payment_methods.count}) + - if enterprise.payment_methods.count == 0 + %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} + %br/ + + - if can? :admin, Spree::ShippingMethod + = link_to_with_icon 'icon-plane', 'Shipping Methods', spree.admin_shipping_methods_path(enterprise_id: enterprise.id) + (#{enterprise.shipping_methods.count}) + - if enterprise.shipping_methods.count == 0 + %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} + %br/ + +- if can? :admin, EnterpriseFee + = link_to_with_icon 'icon-money', 'Enterprise Fees', main_app.admin_enterprise_fees_path(enterprise_id: enterprise.id) + (#{enterprise.enterprise_fees.count}) + - if enterprise.enterprise_fees.count == 0 + %span.icon-warning-sign.with-tip{"data-powertip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} diff --git a/spec/features/admin/enterprise_user_spec.rb b/spec/features/admin/enterprise_user_spec.rb index 399e29d794..a3f35275a1 100644 --- a/spec/features/admin/enterprise_user_spec.rb +++ b/spec/features/admin/enterprise_user_spec.rb @@ -121,7 +121,21 @@ feature %q{ end end - it "shows me only profile options on the enterprises page" + it "shows me only profile options on the enterprise listing page" do + click_link 'Enterprises' + + within "tr.enterprise-#{supplier_profile.id}" do + page.should_not have_link 'Enterprise Fees' + end + + within "tr.enterprise-#{distributor_profile.id}" do + page.should_not have_link 'Payment Methods' + page.should_not have_link 'Shipping Methods' + page.should_not have_link 'Enterprise Fees' + end + end + + it "shows me only profile fields on the enterprise edit page" end describe "system management lockdown" do From 3e97762f7e4cd58d0c927c1a206c0bcc8e9ded02 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 16:23:05 +1000 Subject: [PATCH 156/205] Remove unused producer options on hub tab, make logic consistently ordered --- .../overview/_enterprises_hubs_tab.html.haml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml index 482b03d5e3..139e7883f5 100644 --- a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml @@ -15,21 +15,17 @@ %span.symbol.three.columns.centered - if can? :admin, Spree::PaymentMethod - payment_method_count = enterprise.payment_methods.count - - if payment_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no payment methods" } - - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require payment methods." } - - else + - if payment_method_count > 0 %span.icon-ok-sign.with-tip{ title: "#{pluralize payment_method_count, 'payment method'}" } + - else + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no payment methods" } %span.symbol.three.columns.centered - if can? :admin, Spree::ShippingMethod - shipping_method_count = enterprise.shipping_methods.count - - if shipping_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no shipping methods" } - - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require shipping methods." } - - else + - if shipping_method_count > 0 %span.icon-ok-sign.with-tip{ title: "#{pluralize shipping_method_count, 'shipping method'}" } + - else + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no shipping methods" } %span.symbol.three.columns.centered - if can? :admin, EnterpriseFee - fee_count = enterprise.enterprise_fees.count From 4b42c28bd5d4ff8877729fd795e2755129b04959 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 16:34:56 +1000 Subject: [PATCH 157/205] Refactor admin enterpriseCtrl - variable naming, if x then true else false antipattern --- .../controllers/enterprise_controller.js.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index cf182df494..c5b38191ba 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -4,8 +4,8 @@ angular.module("admin.enterprises") $scope.PaymentMethods = PaymentMethods.paymentMethods $scope.ShippingMethods = ShippingMethods.shippingMethods - for PaymentMethod in $scope.PaymentMethods - PaymentMethod.selected = if PaymentMethod.id in $scope.Enterprise.payment_method_ids then true else false + for payment_method in $scope.PaymentMethods + payment_method.selected = payment_method.id in $scope.Enterprise.payment_method_ids $scope.paymentMethodsColor = -> if $scope.PaymentMethods.length > 0 @@ -14,13 +14,13 @@ angular.module("admin.enterprises") "red" $scope.selectedPaymentMethodsCount = -> - $scope.PaymentMethods.reduce (count, PaymentMethod) -> - count++ if PaymentMethod.selected + $scope.PaymentMethods.reduce (count, payment_method) -> + count++ if payment_method.selected count , 0 - for ShippingMethod in $scope.ShippingMethods - ShippingMethod.selected = if ShippingMethod.id in $scope.Enterprise.shipping_method_ids then true else false + for shipping_method in $scope.ShippingMethods + shipping_method.selected = shipping_method.id in $scope.Enterprise.shipping_method_ids $scope.shippingMethodsColor = -> if $scope.ShippingMethods.length > 0 @@ -29,7 +29,7 @@ angular.module("admin.enterprises") "red" $scope.selectedShippingMethodsCount = -> - $scope.ShippingMethods.reduce (count, ShippingMethod) -> - count++ if ShippingMethod.selected + $scope.ShippingMethods.reduce (count, shipping_method) -> + count++ if shipping_method.selected count , 0 \ No newline at end of file From a533daab96a25017ac8900041e70cc17df2477a2 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 16:41:49 +1000 Subject: [PATCH 158/205] Split sidebar into parts --- .../admin/enterprises/_sidebar.html.haml | 64 +------------------ .../_sidebar_enterprise_fees.html.haml | 20 ++++++ .../_sidebar_payment_methods.html.haml | 21 ++++++ .../_sidebar_shipping_methods.html.haml | 20 ++++++ 4 files changed, 64 insertions(+), 61 deletions(-) create mode 100644 app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml create mode 100644 app/views/admin/enterprises/_sidebar_payment_methods.html.haml create mode 100644 app/views/admin/enterprises/_sidebar_shipping_methods.html.haml diff --git a/app/views/admin/enterprises/_sidebar.html.haml b/app/views/admin/enterprises/_sidebar.html.haml index 7bd228c4c7..c59bb2086d 100644 --- a/app/views/admin/enterprises/_sidebar.html.haml +++ b/app/views/admin/enterprises/_sidebar.html.haml @@ -1,61 +1,3 @@ -.sidebar_item.four.columns.alpha#payment_methods{ ng: { show: 'Enterprise.is_distributor' } } - .four.columns.alpha.header{ ng: { class: "paymentMethodsColor()" } } - %span.four.columns.alpha.centered Payment Methods - .four.columns.alpha.list{ ng: { class: "paymentMethodsColor()" } } - - if @payment_methods.count > 0 - -# = hidden_field_tag "enterprise[payment_method_ids][]", [] - - @payment_methods.each do |payment_method| - %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'paymentMethodCtrl', init: "findPaymentMethodByID(#{payment_method.id})" } } - %a.three.columns.alpha{ href: "#{edit_admin_payment_method_path(payment_method)}" } - = payment_method.name - %span.one.column.omega - = f.check_box :payment_method_ids, { multiple: true, 'ng-model' => 'PaymentMethod.selected' }, payment_method.id, nil - - else - .four.columns.alpha.list-item - %span.three.columns.alpha None Available - %span.one.column.omega - %span.icon-remove-sign - %a.four.columns.alpha.button{ href: "#{new_admin_payment_method_path}", ng: { class: "paymentMethodsColor()" } } - CREATE NEW - %span.icon-arrow-right - -.sidebar_item.four.columns.alpha#shipping_methods{ ng: { show: 'Enterprise.is_distributor' } } - .four.columns.alpha.header{ ng: { class: "shippingMethodsColor()" } } - %span.four.columns.alpha.centered Shipping Methods - .four.columns.alpha.list{ ng: { class: "shippingMethodsColor()" } } - - if @shipping_methods.count > 0 - - @shipping_methods.each do |shipping_method| - %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } - %a.three.columns.alpha{ href: "#{edit_admin_shipping_method_path(shipping_method)}" } - = shipping_method.name - %span.one.column.omega - = f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil - - else - .four.columns.alpha.list-item - %span.three.columns.alpha None Available - %span.one.column.omega - %span.icon-remove-sign - %a.four.columns.alpha.button{ href: "#{new_admin_shipping_method_path}", ng: { class: "shippingMethodsColor()" } } - CREATE NEW - %span.icon-arrow-right - -- enterprise_fees_color = @enterprise_fees.count > 0 ? "blue" : "red" -.sidebar_item.four.columns.alpha#enterprise_fees{ ng: { show: 'Enterprise.is_distributor' } } - .four.columns.alpha.header{ class: "#{enterprise_fees_color}" } - %span.four.columns.alpha.centered Enterprise Fees - .four.columns.alpha.list{ class: "#{enterprise_fees_color}" } - - if @enterprise_fees.count > 0 - - @enterprise_fees.each do |enterprise_fee| - %a.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.admin_enterprise_fees_path}" } - %span.three.columns.alpha - = enterprise_fee.name - %span.one.column.omega -   - - else - .four.columns.alpha.list-item.red - %span.three.columns.alpha None Available - %span.one.column.omega - %span.icon-remove-sign - %a.four.columns.alpha.button{ href: "#{main_app.admin_enterprise_fees_path}", class: "#{enterprise_fees_color}" } - CREATE NEW - %span.icon-arrow-right \ No newline at end of file += render 'sidebar_payment_methods', f: f += render 'sidebar_shipping_methods', f: f += render 'sidebar_enterprise_fees', f: f diff --git a/app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml b/app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml new file mode 100644 index 0000000000..50de7e07d9 --- /dev/null +++ b/app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml @@ -0,0 +1,20 @@ +- enterprise_fees_color = @enterprise_fees.count > 0 ? "blue" : "red" +.sidebar_item.four.columns.alpha#enterprise_fees{ ng: { show: 'Enterprise.is_distributor' } } + .four.columns.alpha.header{ class: "#{enterprise_fees_color}" } + %span.four.columns.alpha.centered Enterprise Fees + .four.columns.alpha.list{ class: "#{enterprise_fees_color}" } + - if @enterprise_fees.count > 0 + - @enterprise_fees.each do |enterprise_fee| + %a.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.admin_enterprise_fees_path}" } + %span.three.columns.alpha + = enterprise_fee.name + %span.one.column.omega +   + - else + .four.columns.alpha.list-item.red + %span.three.columns.alpha None Available + %span.one.column.omega + %span.icon-remove-sign + %a.four.columns.alpha.button{ href: "#{main_app.admin_enterprise_fees_path}", class: "#{enterprise_fees_color}" } + CREATE NEW + %span.icon-arrow-right diff --git a/app/views/admin/enterprises/_sidebar_payment_methods.html.haml b/app/views/admin/enterprises/_sidebar_payment_methods.html.haml new file mode 100644 index 0000000000..8994c60be5 --- /dev/null +++ b/app/views/admin/enterprises/_sidebar_payment_methods.html.haml @@ -0,0 +1,21 @@ +.sidebar_item.four.columns.alpha#payment_methods{ ng: { show: 'Enterprise.is_distributor' } } + .four.columns.alpha.header{ ng: { class: "paymentMethodsColor()" } } + %span.four.columns.alpha.centered Payment Methods + .four.columns.alpha.list{ ng: { class: "paymentMethodsColor()" } } + - if @payment_methods.count > 0 + -# = hidden_field_tag "enterprise[payment_method_ids][]", [] + - @payment_methods.each do |payment_method| + %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'paymentMethodCtrl', init: "findPaymentMethodByID(#{payment_method.id})" } } + %a.three.columns.alpha{ href: "#{edit_admin_payment_method_path(payment_method)}" } + = payment_method.name + %span.one.column.omega + = f.check_box :payment_method_ids, { multiple: true, 'ng-model' => 'PaymentMethod.selected' }, payment_method.id, nil + - else + .four.columns.alpha.list-item + %span.three.columns.alpha None Available + %span.one.column.omega + %span.icon-remove-sign + %a.four.columns.alpha.button{ href: "#{new_admin_payment_method_path}", ng: { class: "paymentMethodsColor()" } } + CREATE NEW + %span.icon-arrow-right + diff --git a/app/views/admin/enterprises/_sidebar_shipping_methods.html.haml b/app/views/admin/enterprises/_sidebar_shipping_methods.html.haml new file mode 100644 index 0000000000..6d4a858366 --- /dev/null +++ b/app/views/admin/enterprises/_sidebar_shipping_methods.html.haml @@ -0,0 +1,20 @@ +.sidebar_item.four.columns.alpha#shipping_methods{ ng: { show: 'Enterprise.is_distributor' } } + .four.columns.alpha.header{ ng: { class: "shippingMethodsColor()" } } + %span.four.columns.alpha.centered Shipping Methods + .four.columns.alpha.list{ ng: { class: "shippingMethodsColor()" } } + - if @shipping_methods.count > 0 + - @shipping_methods.each do |shipping_method| + %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } + %a.three.columns.alpha{ href: "#{edit_admin_shipping_method_path(shipping_method)}" } + = shipping_method.name + %span.one.column.omega + = f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil + - else + .four.columns.alpha.list-item + %span.three.columns.alpha None Available + %span.one.column.omega + %span.icon-remove-sign + %a.four.columns.alpha.button{ href: "#{new_admin_shipping_method_path}", ng: { class: "shippingMethodsColor()" } } + CREATE NEW + %span.icon-arrow-right + From e3b4f6efc1d1885c5d728c67434b3cac326fc081 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 16:43:18 +1000 Subject: [PATCH 159/205] When profile admin only, do not show payment methods, shipping methods or enterprise fees when editing profile --- app/views/admin/enterprises/_ng_form.html.haml | 5 +++-- app/views/admin/enterprises/_sidebar.html.haml | 9 ++++++--- app/views/admin/enterprises/edit.html.haml | 2 +- spec/features/admin/enterprise_user_spec.rb | 8 +++++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 7a4ec13a1f..0ccd963d31 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -1,9 +1,10 @@ = admin_inject_enterprise = admin_inject_payment_methods = admin_inject_shipping_methods + .sixteen.columns.alpha{ ng: { app: 'admin.enterprises', controller: 'enterpriseCtrl' } } .eleven.columns.alpha - = render partial: 'form', :locals => { f: f } + = render 'form', f: f .one.column   .four.columns.omega - = render partial: 'sidebar', :locals => { f: f } \ No newline at end of file + = render 'sidebar', f: f diff --git a/app/views/admin/enterprises/_sidebar.html.haml b/app/views/admin/enterprises/_sidebar.html.haml index c59bb2086d..ac4966fa80 100644 --- a/app/views/admin/enterprises/_sidebar.html.haml +++ b/app/views/admin/enterprises/_sidebar.html.haml @@ -1,3 +1,6 @@ -= render 'sidebar_payment_methods', f: f -= render 'sidebar_shipping_methods', f: f -= render 'sidebar_enterprise_fees', f: f +- if can? :admin, Spree::PaymentMethod + = render 'sidebar_payment_methods', f: f +- if can? :admin, Spree::ShippingMethod + = render 'sidebar_shipping_methods', f: f +- if can? :admin, EnterpriseFee + = render 'sidebar_enterprise_fees', f: f diff --git a/app/views/admin/enterprises/edit.html.haml b/app/views/admin/enterprises/edit.html.haml index 38747a3252..6a8d48b24b 100644 --- a/app/views/admin/enterprises/edit.html.haml +++ b/app/views/admin/enterprises/edit.html.haml @@ -5,6 +5,6 @@ = @enterprise.name = form_for [main_app, :admin, @enterprise] do |f| - = render partial: 'ng_form', :locals => { f: f } + = render 'ng_form', f: f .twelve.columns.alpha = render partial: 'spree/admin/shared/edit_resource_links' diff --git a/spec/features/admin/enterprise_user_spec.rb b/spec/features/admin/enterprise_user_spec.rb index a3f35275a1..ed46a1e0c1 100644 --- a/spec/features/admin/enterprise_user_spec.rb +++ b/spec/features/admin/enterprise_user_spec.rb @@ -135,7 +135,13 @@ feature %q{ end end - it "shows me only profile fields on the enterprise edit page" + it "shows me only profile fields on the hub edit page" do + click_link distributor_profile.name + + page.should_not have_selector '#payment_methods' + page.should_not have_selector '#shipping_methods' + page.should_not have_selector '#enterprise_fees' + end end describe "system management lockdown" do From b35c5e902fa66ca8ad1f4e0c52406de00bec383a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 16:47:51 +1000 Subject: [PATCH 160/205] Fix broken JS specs --- spec/javascripts/application_spec.js | 1 - .../controllers/checkout/checkout_controller_spec.js.coffee | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index d9239c79ad..8c611081a2 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -4,7 +4,6 @@ //= require angular-sanitize //= require angular-mocks //= require angular-cookies -//= require angular-timer.min.js //= require angular-backstretch.js //= require lodash.underscore.js //= require angular-flash.min.js diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee index d56a7495ab..fdbc4742a3 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee @@ -52,7 +52,7 @@ describe "CheckoutCtrl", -> expect(storage.bind).toHaveBeenCalledWith(scope, "Checkout.ship_address_same_as_billing", {storeName: "#{prefix}_sameasbilling", defaultValue: true}) it "it can retrieve data from localstorage", -> - prefix = "order_#{scope.order.id}#{scope.order.user_id}#{CurrentHubMock.hub.id}" + prefix = "order_#{scope.order.id}#{CurrentUser?.id}#{CurrentHubMock.hub.id}" expect(localStorage.getItem("#{prefix}_email")).toMatch "public" it "does not store secrets in local storage", -> From e173c69ee3b2e1ef1aa8a1666ddb6c2fb4220e3e Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 19 Aug 2014 16:53:23 +1000 Subject: [PATCH 161/205] Fix arrow position --- .../spree/admin/overview/_enterprises_hubs_tab.html.haml | 6 ++++++ .../admin/overview/_enterprises_producers_tab.html.haml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml index 139e7883f5..cb177f9fb3 100644 --- a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml @@ -19,6 +19,8 @@ %span.icon-ok-sign.with-tip{ title: "#{pluralize payment_method_count, 'payment method'}" } - else %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no payment methods" } + - else +   %span.symbol.three.columns.centered - if can? :admin, Spree::ShippingMethod - shipping_method_count = enterprise.shipping_methods.count @@ -26,6 +28,8 @@ %span.icon-ok-sign.with-tip{ title: "#{pluralize shipping_method_count, 'shipping method'}" } - else %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no shipping methods" } + - else +   %span.symbol.three.columns.centered - if can? :admin, EnterpriseFee - fee_count = enterprise.enterprise_fees.count @@ -33,5 +37,7 @@ %span.icon-ok-sign.with-tip{ title: "#{pluralize fee_count, 'fee'}" } - else %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no enterprise fees" } + - else +   %span.two.columns.omega.right %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml index 363733710a..d5cad103b7 100644 --- a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml @@ -19,12 +19,16 @@ %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.any? ? "green" : "red" }" } = enterprise.supplied_products.not_deleted.count %span.one.column.omega   + - else +   %span.symbol.three.columns.centered - if can? :admin, Spree::Product %span.one.column.alpha   %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.any? ? "green" : "red" }" } = enterprise.supplied_and_active_products_on_hand.count %span.one.column.omega   + - else +   %span.symbol.three.columns.centered - if can? :admin, OrderCycle @@ -32,6 +36,8 @@ %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.any? ? "green" : "orange" }" } = enterprise.active_products_in_order_cycles.count %span.one.column.omega   + - else +   %span.two.columns.omega.right %span.icon-arrow-right From dd42b0c2399289ae0f6b5794fb754de79e10cc78 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 22 Aug 2014 14:38:44 +1000 Subject: [PATCH 162/205] Split out opening payments into own context --- .../consumer/shopping/checkout_spec.rb | 154 +++++++++--------- 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 846fc57a9b..c35f01f8e0 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -79,53 +79,24 @@ feature "As a consumer I want to check out my cart", js: true do end end - before do - visit checkout_path - checkout_as_guest - toggle_payment - end - - it "shows all available payment methods" do - page.should have_content pm1.name - page.should have_content pm2.name - page.should have_content pm3.name - end - - describe "Purchasing" do - it "takes us to the order confirmation page when we submit a complete form" do - ActionMailer::Base.deliveries.clear - toggle_shipping - choose sm2.name + context "on the checkout page with payments open" do + before do + visit checkout_path + checkout_as_guest toggle_payment - choose pm1.name - toggle_details - within "#details" do - fill_in "First Name", with: "Will" - fill_in "Last Name", with: "Marshall" - fill_in "Email", with: "test@test.com" - fill_in "Phone", with: "0468363090" - end - toggle_billing - within "#billing" do - fill_in "Address", with: "123 Your Face" - select "Australia", from: "Country" - select "Victoria", from: "State" - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" - - end - place_order - page.should have_content "Your order has been processed successfully" - ActionMailer::Base.deliveries.length.should == 1 - email = ActionMailer::Base.deliveries.last - site_name = Spree::Config[:site_name] - email.subject.should include "#{site_name} Order Confirmation" end - context "with basic details filled" do - before do + it "shows all available payment methods" do + page.should have_content pm1.name + page.should have_content pm2.name + page.should have_content pm3.name + end + + describe "Purchasing" do + it "takes us to the order confirmation page when we submit a complete form" do + ActionMailer::Base.deliveries.clear toggle_shipping - choose sm1.name + choose sm2.name toggle_payment choose pm1.name toggle_details @@ -137,52 +108,83 @@ feature "As a consumer I want to check out my cart", js: true do end toggle_billing within "#billing" do - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" fill_in "Address", with: "123 Your Face" select "Australia", from: "Country" select "Victoria", from: "State" - end - toggle_shipping - check "Shipping address same as billing address?" - end + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" - it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do + end place_order page.should have_content "Your order has been processed successfully" + ActionMailer::Base.deliveries.length.should == 1 + email = ActionMailer::Base.deliveries.last + site_name = Spree::Config[:site_name] + email.subject.should include "#{site_name} Order Confirmation" end - context "with a credit card payment method" do - let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::Gateway::Bogus") } - - it "takes us to the order confirmation page when submitted with a valid credit card" do + context "with basic details filled" do + before do + toggle_shipping + choose sm1.name toggle_payment - fill_in 'Card Number', with: "4111111111111111" - select 'February', from: 'secrets.card_month' - select (Date.today.year+1).to_s, from: 'secrets.card_year' - fill_in 'Security Code', with: '123' - - place_order - page.should have_content "Your order has been processed successfully" - - # Order should have a payment with the correct amount - o = Spree::Order.complete.first - o.payments.first.amount.should == 11.23 + choose pm1.name + toggle_details + within "#details" do + fill_in "First Name", with: "Will" + fill_in "Last Name", with: "Marshall" + fill_in "Email", with: "test@test.com" + fill_in "Phone", with: "0468363090" + end + toggle_billing + within "#billing" do + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" + fill_in "Address", with: "123 Your Face" + select "Australia", from: "Country" + select "Victoria", from: "State" + end + toggle_shipping + check "Shipping address same as billing address?" end - it "shows the payment processing failed message when submitted with an invalid credit card" do - toggle_payment - fill_in 'Card Number', with: "9999999988887777" - select 'February', from: 'secrets.card_month' - select (Date.today.year+1).to_s, from: 'secrets.card_year' - fill_in 'Security Code', with: '123' - + it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do place_order - page.should have_content "Payment could not be processed, please check the details you entered" + page.should have_content "Your order has been processed successfully" + end - # Does not show duplicate shipping fee - visit checkout_path - page.all("th", text: "Shipping").count.should == 1 + context "with a credit card payment method" do + let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::Gateway::Bogus") } + + it "takes us to the order confirmation page when submitted with a valid credit card" do + toggle_payment + fill_in 'Card Number', with: "4111111111111111" + select 'February', from: 'secrets.card_month' + select (Date.today.year+1).to_s, from: 'secrets.card_year' + fill_in 'Security Code', with: '123' + + place_order + page.should have_content "Your order has been processed successfully" + + # Order should have a payment with the correct amount + o = Spree::Order.complete.first + o.payments.first.amount.should == 11.23 + end + + it "shows the payment processing failed message when submitted with an invalid credit card" do + toggle_payment + fill_in 'Card Number', with: "9999999988887777" + select 'February', from: 'secrets.card_month' + select (Date.today.year+1).to_s, from: 'secrets.card_year' + fill_in 'Security Code', with: '123' + + place_order + page.should have_content "Payment could not be processed, please check the details you entered" + + # Does not show duplicate shipping fee + visit checkout_path + page.all("th", text: "Shipping").count.should == 1 + end end end end From 4ceaec0ef521bc79a5d06921bec53177de212d01 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 22 Aug 2014 17:34:42 +1000 Subject: [PATCH 163/205] Do not error when checking out with a pre-loaded shipping/billing address --- .../darkswarm/services/checkout.js.coffee | 4 +++ .../consumer/shopping/checkout_spec.rb | 33 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee index 920c01dad9..cafd4d6718 100644 --- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee @@ -32,6 +32,10 @@ Darkswarm.factory 'Checkout', (CurrentOrder, ShippingMethods, PaymentMethods, $h if @ship_address_same_as_billing munged_order.ship_address_attributes = munged_order.bill_address_attributes + # If the order already has a ship and bill address (as with logged in users with + # past orders), and we don't remove id here, then this will set the wrong id for + # ship address, and Rails will error with a 404 for that address. + delete munged_order.ship_address_attributes.id if @paymentMethod()?.method_type == 'gateway' angular.extend munged_order.payments_attributes[0], { diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index c35f01f8e0..54091e854d 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -15,8 +15,8 @@ feature "As a consumer I want to check out my cart", js: true do let(:product) { create(:simple_product, supplier: supplier) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } - before do + ActionMailer::Base.deliveries.clear add_enterprise_fee enterprise_fee set_order order add_product_to_cart @@ -56,7 +56,7 @@ feature "As a consumer I want to check out my cart", js: true do page.should have_content "Donkeys" end - context "When shipping method requires an address" do + context "when shipping method requires an address" do before do toggle_shipping choose sm1.name @@ -92,9 +92,8 @@ feature "As a consumer I want to check out my cart", js: true do page.should have_content pm3.name end - describe "Purchasing" do + describe "purchasing" do it "takes us to the order confirmation page when we submit a complete form" do - ActionMailer::Base.deliveries.clear toggle_shipping choose sm2.name toggle_payment @@ -117,7 +116,7 @@ feature "As a consumer I want to check out my cart", js: true do end place_order page.should have_content "Your order has been processed successfully" - ActionMailer::Base.deliveries.length.should == 1 + ActionMailer::Base.deliveries.length.should == 2 email = ActionMailer::Base.deliveries.last site_name = Spree::Config[:site_name] email.subject.should include "#{site_name} Order Confirmation" @@ -189,6 +188,30 @@ feature "As a consumer I want to check out my cart", js: true do end end end + + context "when the customer has a pre-set shipping and billing address" do + before do + # Load up the customer's order and give them a shipping and billing address + # This is equivalent to when the customer has ordered before and their addresses + # are pre-populated. + o = Spree::Order.last + o.ship_address = build(:address) + o.bill_address = build(:address) + o.save! + end + + it "checks out successfully" do + visit checkout_path + checkout_as_guest + choose sm2.name + toggle_payment + choose pm1.name + + place_order + page.should have_content "Your order has been processed successfully" + ActionMailer::Base.deliveries.length.should == 2 + end + end end end end From 3524e658f85842cf721d078f500ad9334c129cda Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 11:52:15 +1000 Subject: [PATCH 164/205] Error when creating product and master is invalid, instead of creating a product without a master --- app/models/spree/product_decorator.rb | 1 + app/models/spree/variant_decorator.rb | 6 +++--- spec/models/spree/product_spec.rb | 10 +++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 4faa54a06d..d344e19935 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -17,6 +17,7 @@ Spree::Product.class_eval do attr_accessible :supplier_id, :primary_taxon_id, :distributor_ids, :product_distributions_attributes, :group_buy, :group_buy_unit_size attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value, :unit_description, :notes, :images_attributes, :display_as + validates_associated :master, message: "^Price and On Hand must be valid" validates_presence_of :supplier validates :primary_taxon, presence: { message: "^Product Category can't be blank" } diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 7d4f178488..29d2ed89c1 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -9,14 +9,14 @@ Spree::Variant.class_eval do accepts_nested_attributes_for :images validates_presence_of :unit_value, - if: -> v { %w(weight volume).include? v.product.variant_unit }, + if: -> v { %w(weight volume).include? v.product.andand.variant_unit }, unless: :is_master validates_presence_of :unit_description, - if: -> v { v.product.variant_unit.present? && v.unit_value.nil? }, + if: -> v { v.product.andand.variant_unit.present? && v.unit_value.nil? }, unless: :is_master - before_validation :update_weight_from_unit_value + before_validation :update_weight_from_unit_value, if: -> v { v.product.present? } after_save :update_units scope :with_order_cycles_inner, joins(exchanges: :order_cycle) diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index bba8a95301..f82d07674f 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -27,12 +27,20 @@ module Spree product.should_not be_valid end - it "should default available_on to now" do + it "defaults available_on to now" do Timecop.freeze product = Product.new product.available_on.should == Time.now end + it "does not save when master is invalid" do + s = create(:supplier_enterprise) + t = create(:taxon) + product = Product.new supplier_id: s.id, name: "Apples", price: 1, primary_taxon_id: t.id + product.on_hand = "10,000" + product.save.should be_false + end + context "when the product has variants" do let(:product) do product = create(:simple_product) From cb615ba994d1bf268cdd0dd5b973a271cf242540 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 13:07:31 +1000 Subject: [PATCH 165/205] Render enterprise relationships JSON with AMS instead of rabl --- .../admin/enterprise_relationships_controller.rb | 2 +- app/helpers/admin/injection_helper.rb | 4 ++++ .../api/admin/enterprise_relationship_serializer.rb | 11 +++++++++++ .../admin/enterprise_relationships/_data.html.haml | 6 ++---- app/views/admin/json/_enterprise_relationship.rabl | 11 ----------- app/views/admin/json/_enterprise_relationships.rabl | 2 -- 6 files changed, 18 insertions(+), 18 deletions(-) create mode 100644 app/serializers/api/admin/enterprise_relationship_serializer.rb delete mode 100644 app/views/admin/json/_enterprise_relationship.rabl delete mode 100644 app/views/admin/json/_enterprise_relationships.rabl diff --git a/app/controllers/admin/enterprise_relationships_controller.rb b/app/controllers/admin/enterprise_relationships_controller.rb index 212bf3849d..7ef435d7ed 100644 --- a/app/controllers/admin/enterprise_relationships_controller.rb +++ b/app/controllers/admin/enterprise_relationships_controller.rb @@ -10,7 +10,7 @@ module Admin @enterprise_relationship = EnterpriseRelationship.new params[:enterprise_relationship] if @enterprise_relationship.save - render partial: "admin/json/enterprise_relationship", locals: {enterprise_relationship: @enterprise_relationship} + render text: Api::Admin::EnterpriseRelationshipSerializer.new(@enterprise_relationship).to_json else render status: 400, json: {errors: @enterprise_relationship.errors.full_messages.join(', ')} end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 60bc911087..9ce62b2719 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -9,6 +9,10 @@ module Admin admin_inject_json_ams_array("ofn.admin", "all_enterprises", @all_enterprises, Api::Admin::EnterpriseSerializer) end + def admin_inject_enterprise_relationships + admin_inject_json_ams_array "ofn.admin", "enterprise_relationships", @enterprise_relationships, Api::Admin::EnterpriseRelationshipSerializer + end + def admin_inject_enterprise_roles admin_inject_json_ams_array "ofn.admin", "enterpriseRoles", @enterprise_roles, Api::Admin::EnterpriseRoleSerializer end diff --git a/app/serializers/api/admin/enterprise_relationship_serializer.rb b/app/serializers/api/admin/enterprise_relationship_serializer.rb new file mode 100644 index 0000000000..708920d8f5 --- /dev/null +++ b/app/serializers/api/admin/enterprise_relationship_serializer.rb @@ -0,0 +1,11 @@ +class Api::Admin::EnterpriseRelationshipSerializer < ActiveModel::Serializer + attributes :id, :parent_id, :parent_name, :child_id, :child_name + + def parent_name + object.parent.name + end + + def child_name + object.child.name + end +end diff --git a/app/views/admin/enterprise_relationships/_data.html.haml b/app/views/admin/enterprise_relationships/_data.html.haml index 7c13978e90..b6f7215afc 100644 --- a/app/views/admin/enterprise_relationships/_data.html.haml +++ b/app/views/admin/enterprise_relationships/_data.html.haml @@ -1,4 +1,2 @@ -:javascript - angular.module('ofn.admin').value('enterprise_relationships', #{render partial: "admin/json/enterprise_relationships", object: @enterprise_relationships}); - angular.module('ofn.admin').value('my_enterprises', #{render partial: "admin/json/enterprises", object: @my_enterprises}); - angular.module('ofn.admin').value('all_enterprises', #{render partial: "admin/json/enterprises", object: @all_enterprises}); += admin_inject_enterprise_relationships += admin_inject_enterprises diff --git a/app/views/admin/json/_enterprise_relationship.rabl b/app/views/admin/json/_enterprise_relationship.rabl deleted file mode 100644 index 9be152ec5c..0000000000 --- a/app/views/admin/json/_enterprise_relationship.rabl +++ /dev/null @@ -1,11 +0,0 @@ -object @enterprise_relationship - -attributes :id, :parent_id, :child_id - -node :parent_name do |enterprise_relationship| - enterprise_relationship.parent.name -end - -node :child_name do |enterprise_relationship| - enterprise_relationship.child.name -end diff --git a/app/views/admin/json/_enterprise_relationships.rabl b/app/views/admin/json/_enterprise_relationships.rabl deleted file mode 100644 index aad55b9770..0000000000 --- a/app/views/admin/json/_enterprise_relationships.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @enterprise_relationships -extends "admin/json/enterprise_relationship" From 70feef1256d341407d810e27f8726b306b1acb3d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 13:26:23 +1000 Subject: [PATCH 166/205] Add EnterpriseRelationshipPermission model --- app/models/enterprise_relationship.rb | 1 + app/models/enterprise_relationship_permission.rb | 2 ++ ...3227_create_enterprise_relationship_permissions.rb | 11 +++++++++++ db/schema.rb | 11 ++++++++++- spec/factories.rb | 6 ++++++ 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 app/models/enterprise_relationship_permission.rb create mode 100644 db/migrate/20140825023227_create_enterprise_relationship_permissions.rb diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 4db650f2e7..312ff66572 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -1,6 +1,7 @@ class EnterpriseRelationship < ActiveRecord::Base belongs_to :parent, class_name: 'Enterprise', touch: true belongs_to :child, class_name: 'Enterprise', touch: true + has_many :permissions, class_name: 'EnterpriseRelationshipPermission' validates_presence_of :parent_id, :child_id validates_uniqueness_of :child_id, scope: :parent_id, message: "^That relationship is already established." diff --git a/app/models/enterprise_relationship_permission.rb b/app/models/enterprise_relationship_permission.rb new file mode 100644 index 0000000000..f615a91da7 --- /dev/null +++ b/app/models/enterprise_relationship_permission.rb @@ -0,0 +1,2 @@ +class EnterpriseRelationshipPermission < ActiveRecord::Base +end diff --git a/db/migrate/20140825023227_create_enterprise_relationship_permissions.rb b/db/migrate/20140825023227_create_enterprise_relationship_permissions.rb new file mode 100644 index 0000000000..1423c09db9 --- /dev/null +++ b/db/migrate/20140825023227_create_enterprise_relationship_permissions.rb @@ -0,0 +1,11 @@ +class CreateEnterpriseRelationshipPermissions < ActiveRecord::Migration + def change + create_table :enterprise_relationship_permissions do |t| + t.references :enterprise_relationship + t.string :name, null: false + end + + add_index :enterprise_relationship_permissions, :enterprise_relationship_id, name: 'index_erp_on_erid' + add_foreign_key :enterprise_relationship_permissions, :enterprise_relationships, name: 'erp_enterprise_relationship_id_fk' + end +end diff --git a/db/schema.rb b/db/schema.rb index 2c9c6b24ff..316c341576 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 => 20140815065014) do +ActiveRecord::Schema.define(:version => 20140825023227) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -207,6 +207,13 @@ ActiveRecord::Schema.define(:version => 20140815065014) do add_index "enterprise_groups_enterprises", ["enterprise_group_id"], :name => "index_enterprise_groups_enterprises_on_enterprise_group_id" add_index "enterprise_groups_enterprises", ["enterprise_id"], :name => "index_enterprise_groups_enterprises_on_enterprise_id" + create_table "enterprise_relationship_permissions", :force => true do |t| + t.integer "enterprise_relationship_id" + t.string "name", :null => false + end + + add_index "enterprise_relationship_permissions", ["enterprise_relationship_id"], :name => "index_erp_on_erid" + create_table "enterprise_relationships", :force => true do |t| t.integer "parent_id" t.integer "child_id" @@ -1052,6 +1059,8 @@ ActiveRecord::Schema.define(:version => 20140815065014) do add_foreign_key "enterprise_groups_enterprises", "enterprise_groups", name: "enterprise_groups_enterprises_enterprise_group_id_fk" add_foreign_key "enterprise_groups_enterprises", "enterprises", name: "enterprise_groups_enterprises_enterprise_id_fk" + add_foreign_key "enterprise_relationship_permissions", "enterprise_relationships", name: "erp_enterprise_relationship_id_fk" + add_foreign_key "enterprise_relationships", "enterprises", name: "enterprise_relationships_child_id_fk", column: "child_id" add_foreign_key "enterprise_relationships", "enterprises", name: "enterprise_relationships_parent_id_fk", column: "parent_id" diff --git a/spec/factories.rb b/spec/factories.rb index 849d38abf3..39b3a01035 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -101,6 +101,12 @@ FactoryGirl.define do end factory :enterprise_relationship do + ignore { permissions [] } + after(:create) do |er, proxy| + proxy.permissions.each do |name| + er.permissions.create! name: name + end + end end factory :enterprise_role do From b4e89ad2c0a07e0632697eb21e5b66e2041a9276 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 13:27:01 +1000 Subject: [PATCH 167/205] Convert enterprise relationship permission to string presentation --- .../services/enterprise_relationships.js.coffee | 5 +++++ .../enterprise_relationships_spec.js.coffee | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index e07b992112..3185f229dd 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -16,3 +16,8 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris delete: (er) -> $http.delete('/admin/enterprise_relationships/' + er.id).success (data) => @enterprise_relationships.splice @enterprise_relationships.indexOf(er), 1 + + permission_presentation: (permission) -> + switch permission + when "add_products_to_order_cycle" then "can add products to order cycle from" + when "manage_products" then "can manage the products of" \ No newline at end of file diff --git a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee new file mode 100644 index 0000000000..4e90f65335 --- /dev/null +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -0,0 +1,16 @@ +describe "enterprise relationships", -> + EnterpriseRelationships = null + enterprise_relationships = [] + + beforeEach -> + module "ofn.admin" + module ($provide) -> + $provide.value "enterprise_relationships", enterprise_relationships + null + + beforeEach inject (_EnterpriseRelationships_) -> + EnterpriseRelationships = _EnterpriseRelationships_ + + it "presents permission names", -> + expect(EnterpriseRelationships.permission_presentation("add_products_to_order_cycle")).toEqual "can add products to order cycle from" + expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "can manage the products of" From 50c559964ce1791c78e7f3cad3b6ffafdb1caf29 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 13:28:02 +1000 Subject: [PATCH 168/205] Display enterprise relationship permissions --- ...rprise_relationship_permission_serializer.rb | 3 +++ .../admin/enterprise_relationship_serializer.rb | 2 ++ .../_enterprise_relationship.html.haml | 5 ++++- .../admin/enterprise_relationships_spec.rb | 17 +++++++++++------ 4 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 app/serializers/api/admin/enterprise_relationship_permission_serializer.rb diff --git a/app/serializers/api/admin/enterprise_relationship_permission_serializer.rb b/app/serializers/api/admin/enterprise_relationship_permission_serializer.rb new file mode 100644 index 0000000000..80ce487738 --- /dev/null +++ b/app/serializers/api/admin/enterprise_relationship_permission_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::EnterpriseRelationshipPermissionSerializer < ActiveModel::Serializer + attributes :id, :name +end diff --git a/app/serializers/api/admin/enterprise_relationship_serializer.rb b/app/serializers/api/admin/enterprise_relationship_serializer.rb index 708920d8f5..5030620384 100644 --- a/app/serializers/api/admin/enterprise_relationship_serializer.rb +++ b/app/serializers/api/admin/enterprise_relationship_serializer.rb @@ -1,6 +1,8 @@ class Api::Admin::EnterpriseRelationshipSerializer < ActiveModel::Serializer attributes :id, :parent_id, :parent_name, :child_id, :child_name + has_many :permissions + def parent_name object.parent.name end diff --git a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml index 3f93027ca1..56f0d46c01 100644 --- a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml +++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml @@ -1,5 +1,8 @@ %td {{ enterprise_relationship.parent_name }} -%td permits +%td + %ul + %li{"ng-repeat" => "permission in enterprise_relationship.permissions"} + {{ EnterpriseRelationships.permission_presentation(permission.name) }} %td {{ enterprise_relationship.child_name }} %td.actions %a.delete-enterprise-relationship.icon-trash.no-text{'ng-click' => 'delete(enterprise_relationship)'} diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 1443cd4c34..112529376a 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -14,8 +14,9 @@ feature %q{ scenario "listing relationships" do # Given some enterprises with relationships e1, e2, e3, e4 = create(:enterprise), create(:enterprise), create(:enterprise), create(:enterprise) - create(:enterprise_relationship, parent: e1, child: e2) - create(:enterprise_relationship, parent: e3, child: e4) + create(:enterprise_relationship, parent: e1, child: e2, permissions: [:add_products_to_order_cycle]) + create(:enterprise_relationship, parent: e2, child: e3, permissions: [:manage_products]) + create(:enterprise_relationship, parent: e3, child: e4, permissions: [:add_products_to_order_cycle, :manage_products]) # When I go to the relationships page click_link 'Enterprises' @@ -23,8 +24,10 @@ feature %q{ # Then I should see the relationships within('table#enterprise-relationships') do - page.should have_relationship e1, e2 - page.should have_relationship e3, e4 + page.should have_relationship e1, e2, ['can add products to order cycle from'] + page.should have_relationship e2, e3, ['can manage the products of'] + page.should have_relationship e3, e4, + ['can add products to order cycle from', 'can manage the products of'] end end @@ -108,7 +111,9 @@ feature %q{ private - def have_relationship(parent, child) - have_table_row [parent.name, 'permits', child.name, ''] + def have_relationship(parent, child, perms=[]) + perms = perms.join(' ') || 'permits' + + have_table_row [parent.name, perms, child.name, ''] end end From 057ad9c6d3d54517123fba24b896dd6508e5ae35 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 14:39:24 +1000 Subject: [PATCH 169/205] Set enterprise relationship permissions from a list --- app/models/enterprise_relationship.rb | 5 +++++ spec/models/enterprise_relationship_spec.rb | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 312ff66572..5c10aafbbf 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -14,4 +14,9 @@ class EnterpriseRelationship < ActiveRecord::Base scope :involving_enterprises, ->(enterprises) { where('parent_id IN (?) OR child_id IN (?)', enterprises, enterprises) } + + + def permissions_list=(perms) + perms.andand.each { |name| permissions.build name: name } + end end diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index b715674594..6cdf3db657 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -29,5 +29,19 @@ describe EnterpriseRelationship do EnterpriseRelationship.involving_enterprises([e3]).should == [] end end + + describe "creating with a permission list" do + it "creates permissions with a list" do + er = EnterpriseRelationship.create! parent: e1, child: e2, permissions_list: ['one', 'two'] + er.reload + er.permissions.map(&:name).sort.should == ['one', 'two'].sort + end + + it "does nothing when the list is nil" do + er = EnterpriseRelationship.create! parent: e1, child: e2, permissions_list: nil + er.reload + er.permissions.should be_empty + end + end end end From 3932884dba98544e311d224498ca63b67eb10250 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 14:59:10 +1000 Subject: [PATCH 170/205] Admin can create enterprise relationships with permissions --- .../enterprise_relationships_controller.js.coffee | 3 ++- .../admin/services/enterprise_relationships.js.coffee | 9 +++++++-- app/views/admin/enterprise_relationships/_form.html.haml | 6 +++++- spec/features/admin/enterprise_relationships_spec.rb | 9 +++++++-- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee index 665753a522..88524fb330 100644 --- a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee @@ -1,9 +1,10 @@ angular.module("ofn.admin").controller "AdminEnterpriseRelationshipsCtrl", ($scope, EnterpriseRelationships, Enterprises) -> $scope.EnterpriseRelationships = EnterpriseRelationships $scope.Enterprises = Enterprises + $scope.permissions = {} $scope.create = -> - $scope.EnterpriseRelationships.create($scope.parent_id, $scope.child_id) + $scope.EnterpriseRelationships.create($scope.parent_id, $scope.child_id, $scope.permissions) $scope.delete = (enterprise_relationship) -> if confirm("Are you sure?") diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index 3185f229dd..7a799f2f28 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -1,12 +1,17 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterprise_relationships) -> new class EnterpriseRelationships create_errors: "" + all_permissions: [ + 'add_products_to_order_cycle' + 'manage_products' + ] constructor: -> @enterprise_relationships = enterprise_relationships - create: (parent_id, child_id) -> - $http.post('/admin/enterprise_relationships', {enterprise_relationship: {parent_id: parent_id, child_id: child_id}}).success (data, status) => + create: (parent_id, child_id, permissions) -> + permissions = (name for name, enabled of permissions when enabled) + $http.post('/admin/enterprise_relationships', {enterprise_relationship: {parent_id: parent_id, child_id: child_id, permissions_list: permissions}}).success (data, status) => @enterprise_relationships.unshift(data) @create_errors = "" diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index 1a737ec3a5..abb96f3c8f 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -1,7 +1,11 @@ %tr %td %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} - %td permits + %td + %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} + %label + %input{type: "checkbox", "ng-model" => "permissions[permission]"} + {{ EnterpriseRelationships.permission_presentation(permission) }} %td %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} %td.actions diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 112529376a..7eadf37ffa 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -38,11 +38,16 @@ feature %q{ visit admin_enterprise_relationships_path select 'One', from: 'enterprise_relationship_parent_id' + check 'can add products to order cycle from' + check 'can manage the products of' + uncheck 'can manage the products of' select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2 - EnterpriseRelationship.where(parent_id: e1, child_id: e2).should be_present + page.should have_relationship e1, e2, ['can add products to order cycle from'] + er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first + er.should be_present + er.permissions.map(&:name).should == ['add_products_to_order_cycle'] end From c3224ce668a1ab61cceb76f91477ab386dc1b6fc Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 15:05:49 +1000 Subject: [PATCH 171/205] Style permission list items, order perms consistently by name --- app/assets/stylesheets/admin/relationships.css.sass | 3 +++ app/models/enterprise_relationship_permission.rb | 1 + 2 files changed, 4 insertions(+) diff --git a/app/assets/stylesheets/admin/relationships.css.sass b/app/assets/stylesheets/admin/relationships.css.sass index a6467958b7..a5b8879125 100644 --- a/app/assets/stylesheets/admin/relationships.css.sass +++ b/app/assets/stylesheets/admin/relationships.css.sass @@ -8,6 +8,9 @@ table#enterprise-relationships, table#enterprise-roles + ul + list-style-type: none + th.actions, td.actions width: 16% .errors diff --git a/app/models/enterprise_relationship_permission.rb b/app/models/enterprise_relationship_permission.rb index f615a91da7..0833c1386e 100644 --- a/app/models/enterprise_relationship_permission.rb +++ b/app/models/enterprise_relationship_permission.rb @@ -1,2 +1,3 @@ class EnterpriseRelationshipPermission < ActiveRecord::Base + default_scope order('name') end From 1a995aeddabfdb4be9fcbe50f48276b9ceae1127 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 15:20:46 +1000 Subject: [PATCH 172/205] Simplify enterprise_relationship factory - leverage permissions_list= model method --- spec/factories.rb | 6 ------ spec/features/admin/enterprise_relationships_spec.rb | 6 +++--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index 39b3a01035..849d38abf3 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -101,12 +101,6 @@ FactoryGirl.define do end factory :enterprise_relationship do - ignore { permissions [] } - after(:create) do |er, proxy| - proxy.permissions.each do |name| - er.permissions.create! name: name - end - end end factory :enterprise_role do diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 7eadf37ffa..20c7b28ac2 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -14,9 +14,9 @@ feature %q{ scenario "listing relationships" do # Given some enterprises with relationships e1, e2, e3, e4 = create(:enterprise), create(:enterprise), create(:enterprise), create(:enterprise) - create(:enterprise_relationship, parent: e1, child: e2, permissions: [:add_products_to_order_cycle]) - create(:enterprise_relationship, parent: e2, child: e3, permissions: [:manage_products]) - create(:enterprise_relationship, parent: e3, child: e4, permissions: [:add_products_to_order_cycle, :manage_products]) + create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:add_products_to_order_cycle]) + create(:enterprise_relationship, parent: e2, child: e3, permissions_list: [:manage_products]) + create(:enterprise_relationship, parent: e3, child: e4, permissions_list: [:add_products_to_order_cycle, :manage_products]) # When I go to the relationships page click_link 'Enterprises' From 45a44844ca0cba0bc893edc2aa566d472ded5fb3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 25 Aug 2014 16:38:17 +1000 Subject: [PATCH 173/205] Remove old rabl spec --- .../enterprise_relationships_rabl_spec.rb | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 spec/views/admin/json/enterprise_relationships_rabl_spec.rb diff --git a/spec/views/admin/json/enterprise_relationships_rabl_spec.rb b/spec/views/admin/json/enterprise_relationships_rabl_spec.rb deleted file mode 100644 index 9b72a0d45b..0000000000 --- a/spec/views/admin/json/enterprise_relationships_rabl_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe "admin/json/_enterprise_relationships.json.rabl" do - let(:parent) { create(:enterprise) } - let(:child) { create(:enterprise) } - let(:enterprise_relationship) { create(:enterprise_relationship, parent: parent, child: child) } - let(:render) { Rabl.render([enterprise_relationship], 'admin/json/enterprise_relationships', view_path: 'app/views', scope: RablHelper::FakeContext.instance) } - - it "renders a list of enterprise relationships" do - render.should have_json_type(Array).at_path '' - render.should have_json_type(Object).at_path '0' - end - - it "renders enterprise ids" do - render.should be_json_eql(parent.id).at_path '0/parent_id' - render.should be_json_eql(child.id).at_path '0/child_id' - end - - it "renders enterprise names" do - render.should be_json_eql(parent.name.to_json).at_path '0/parent_name' - render.should be_json_eql(child.name.to_json).at_path '0/child_name' - end -end From 0d9e07d4849503402ea701a431b746024e15094c Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 15 Aug 2014 15:10:44 +1000 Subject: [PATCH 174/205] Make restore script compatible with OSX --- script/restore.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/restore.sh b/script/restore.sh index 00457225ac..ea87a84631 100755 --- a/script/restore.sh +++ b/script/restore.sh @@ -6,4 +6,4 @@ set -e echo "drop database open_food_network_dev" | psql -h localhost -U ofn open_food_network_test echo "create database open_food_network_dev" | psql -h localhost -U ofn open_food_network_test -zcat $1 |psql -h localhost -U ofn open_food_network_dev +gunzip -c $1 |psql -h localhost -U ofn open_food_network_dev From 310d1b37260f49588c4302eaf529d5834005adb9 Mon Sep 17 00:00:00 2001 From: Rob H Date: Fri, 15 Aug 2014 18:15:01 +1000 Subject: [PATCH 175/205] Zeus server does not crash when editing controllers --- app/controllers/spree/admin/reports_controller_decorator.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 28174724dd..18d9bffbdd 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -4,11 +4,8 @@ require 'open_food_network/products_and_inventory_report' require 'open_food_network/group_buy_report' require 'open_food_network/order_grouper' require 'open_food_network/customers_report' -require 'open_food_network/model_class_from_controller_name' Spree::Admin::ReportsController.class_eval do - include OpenFoodNetwork::ModelClassFromControllerName - # Fetches user's distributors, suppliers and order_cycles before_filter :load_data, only: [:customers, :products_and_inventory] From 435819acc4b8ee702242fe3f88903f99f2352eff Mon Sep 17 00:00:00 2001 From: Rob H Date: Mon, 25 Aug 2014 17:57:33 +1000 Subject: [PATCH 176/205] Removing unit text from total units column --- .../admin/reports_controller_decorator.rb | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 18d9bffbdd..0abfd783b5 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -6,7 +6,7 @@ require 'open_food_network/order_grouper' require 'open_food_network/customers_report' Spree::Admin::ReportsController.class_eval do - # Fetches user's distributors, suppliers and order_cycles + # Fetches user's distributors, suppliers and order_cycles before_filter :load_data, only: [:customers, :products_and_inventory] # Render a partial for orders and fulfillment description @@ -362,9 +362,9 @@ Spree::Admin::ReportsController.class_eval do lis end.flatten #payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments - + # -- Prepare form options - my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) + my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) my_suppliers = Enterprise.is_primary_producer.managed_by(spree_current_user) # My distributors and any distributors distributing products I supply @@ -385,21 +385,11 @@ Spree::Admin::ReportsController.class_eval do header = ["Producer", "Product", "Variant", "Amount", "Total Units", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] - ovn = OpenFoodNetwork::OptionValueNamer.new() - columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, proc { |line_items| line_items.first.variant.product.name }, proc { |line_items| line_items.first.variant.full_name }, proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| ovn.name(OpenStruct.new({ - unit_value: ( line_items.map{ |li| li.variant.unit_value.nil? }.any? ? 0 : line_items.sum { |li| li.quantity * li.variant.unit_value } ), - unit_description: line_items.first.variant.unit_description, - product: OpenStruct.new({ - variant_unit: line_items.first.product.variant_unit, - variant_unit_scale: line_items.first.product.variant_unit_scale, - variant_unit_name: line_items.first.product.variant_unit_name - }) - }))}, + proc { |line_items| total_units(line_items) }, proc { |line_items| line_items.first.variant.price }, proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, proc { |line_items| "" }, @@ -604,10 +594,10 @@ Spree::Admin::ReportsController.class_eval do def load_data my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) my_suppliers = Enterprise.is_primary_producer.managed_by(spree_current_user) - distributors_of_my_products = Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers)) - @distributors = my_distributors | distributors_of_my_products + distributors_of_my_products = Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers)) + @distributors = my_distributors | distributors_of_my_products suppliers_of_products_I_distribute = my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq - @suppliers = my_suppliers | suppliers_of_products_I_distribute + @suppliers = my_suppliers | suppliers_of_products_I_distribute @order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC') end @@ -625,4 +615,13 @@ Spree::Admin::ReportsController.class_eval do end reports end + + def total_units(line_items) + return " " if line_items.map{ |li| li.variant.unit_value.nil? }.any? + total_units = line_items.sum do |li| + scale_factor = ( li.product.variant_unit == 'weight' ? 1000 : 1 ) + li.quantity * li.variant.unit_value / scale_factor + end + total_units.round(3) + end end From 0462b3e55d83084c8f21f4b94d1eec8fbbfe2f58 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 14:40:34 +1000 Subject: [PATCH 177/205] Prevent duplicate enterprise roles --- .../20140826043521_prevent_duplicate_enterprise_roles.rb | 5 +++++ db/schema.rb | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20140826043521_prevent_duplicate_enterprise_roles.rb diff --git a/db/migrate/20140826043521_prevent_duplicate_enterprise_roles.rb b/db/migrate/20140826043521_prevent_duplicate_enterprise_roles.rb new file mode 100644 index 0000000000..5b4fc6e7fc --- /dev/null +++ b/db/migrate/20140826043521_prevent_duplicate_enterprise_roles.rb @@ -0,0 +1,5 @@ +class PreventDuplicateEnterpriseRoles < ActiveRecord::Migration + def change + add_index :enterprise_roles, [:enterprise_id, :user_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 316c341576..dbb50d6b33 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 => 20140825023227) do +ActiveRecord::Schema.define(:version => 20140826043521) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -228,6 +228,7 @@ ActiveRecord::Schema.define(:version => 20140825023227) do t.integer "enterprise_id" end + add_index "enterprise_roles", ["enterprise_id", "user_id"], :name => "index_enterprise_roles_on_enterprise_id_and_user_id", :unique => true add_index "enterprise_roles", ["enterprise_id"], :name => "index_enterprise_roles_on_enterprise_id" add_index "enterprise_roles", ["user_id", "enterprise_id"], :name => "index_enterprise_roles_on_user_id_and_enterprise_id", :unique => true add_index "enterprise_roles", ["user_id"], :name => "index_enterprise_roles_on_user_id" @@ -562,9 +563,9 @@ ActiveRecord::Schema.define(:version => 20140825023227) 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 From 400f2ea9b9a72469135e34bfe28f4bbbaa8f1360 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 27 Aug 2014 15:09:41 +1000 Subject: [PATCH 178/205] Don't add payment forms to checkout DOM unless required --- .../checkout/payment_controller.js.coffee | 4 ++-- .../darkswarm/mixins/fieldset_mixin.js.coffee | 9 ++++----- app/views/checkout/_form.html.haml | 10 +++++----- app/views/checkout/_payment.html.haml | 12 ++++++------ app/views/checkout/edit.html.haml | 8 ++++---- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee index 9ae7c14b90..71f893aa64 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee @@ -6,7 +6,7 @@ Darkswarm.controller "PaymentCtrl", ($scope, $timeout) -> {key: "January", value: "1"}, {key: "February", value: "2"}, {key: "March", value: "3"}, - {key: "April", value: "4"}, + {key: "April", value: "4"}, {key: "May", value: "5"}, {key: "June", value: "6"}, {key: "July", value: "7"}, @@ -20,4 +20,4 @@ Darkswarm.controller "PaymentCtrl", ($scope, $timeout) -> $scope.years = [moment().year()..(moment().year()+15)] $scope.secrets.card_month = "1" $scope.secrets.card_year = moment().year() - $timeout $scope.onTimeout + $timeout $scope.onTimeout diff --git a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee index 26b7ac3518..b9f59e93a8 100644 --- a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee +++ b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee @@ -1,7 +1,7 @@ window.FieldsetMixin = ($scope)-> $scope.next = (event = false)-> event.preventDefault() if event - $scope.show $scope.nextPanel + $scope.show $scope.nextPanel $scope.onTimeout = -> if $scope[$scope.name].$valid @@ -36,7 +36,6 @@ window.FieldsetMixin = ($scope)-> when "number" then "must be number" when "email" then "must be email address" - #server_errors = $scope.Order.errors[path.replace('order.', '')] - #errors.push server_errors if server_errors? - (errors.filter (error) -> error?).join ", " - + #server_errors = $scope.Order.errors[path.replace('order.', '')] + #errors.push server_errors if server_errors? + (errors.filter (error) -> error?).join ", " \ No newline at end of file diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index aa2312ccee..a64be0ac44 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -1,5 +1,5 @@ -= f_form_for current_order, url: main_app.update_checkout_path, - html: {name: "checkout", += f_form_for current_order, url: main_app.update_checkout_path, + html: {name: "checkout", id: "checkout_form", novalidate: true, name: "checkout"} do |f| @@ -10,8 +10,8 @@ angular.module('Darkswarm').value('order', #{render "checkout/order"}) %div - / %h3.text-center.pad-top - / Checkout from + / %h3.text-center.pad-top + / Checkout from / = current_distributor.name = render partial: "checkout/details", locals: {f: f} @@ -19,7 +19,7 @@ = render partial: "checkout/shipping", locals: {f: f} = render partial: "checkout/payment", locals: {f: f} %p - %button.button.primary{type: :submit, + %button.button.primary{type: :submit, "ng-click" => "purchase($event)", "ng-disabled" => "checkout.$invalid"} Place order now diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index e6a0fd373c..b655659738 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -1,7 +1,7 @@ %fieldset#payment %ng-form{"ng-controller" => "PaymentCtrl", name: "payment"} - %h5{"ng-class" => "{valid: payment.$valid, dirty: payment.$dirty}"} + %h5{"ng-class" => "{valid: payment.$valid, dirty: payment.$dirty}"} %span.right %label.label.round.alert.right %i.ofn-i_009-close @@ -14,16 +14,16 @@ %accordion-heading .row .small-8.medium-10.columns - %em + %em %small {{ Checkout.paymentMethod().name }} .small-4.medium-2.columns.text-right %span.accordion-up - %em + %em %small Hide - %i.ofn-i_053-point-up + %i.ofn-i_053-point-up %span.accordion-down - %em + %em %small Expand %i.ofn-i_052-point-down @@ -38,7 +38,7 @@ "ng-model" => "order.payment_method_id" = method.name - .row{"ng-show" => "order.payment_method_id == #{method.id}"} + .row{"ng-if" => "order.payment_method_id == #{method.id}"} .small-12.columns = render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index 7fffdbeb22..2744627d03 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -1,16 +1,16 @@ -= inject_enterprises += inject_enterprises .darkswarm - content_for :order_cycle_form do - + %closing Checkout now - %p + %p Order ready for %strong = pickup_time current_order_cycle = render partial: "shopping_shared/details" - + %accordion{"close-others" => "true"} %checkout.row{"ng-controller" => "CheckoutCtrl"} .small-12.medium-8.large-9.columns From 7124dc57fdb1ee98b4e290ca09afd5fecc03e3d2 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 27 Aug 2014 16:59:26 +1000 Subject: [PATCH 179/205] Requiring a state in checkout --- app/views/checkout/_billing.html.haml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index 2fceedb523..1e7adec1ad 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -19,13 +19,13 @@ {{ summary() | printArray }} .small-4.medium-2.columns.text-right %span.accordion-up - %em + %em %small Hide - %i.ofn-i_053-point-up + %i.ofn-i_053-point-up %span.accordion-down - %em + %em %small Expand - %i.ofn-i_052-point-down + %i.ofn-i_052-point-down = f.fields_for :bill_address, @order.bill_address do |ba| .row @@ -40,7 +40,7 @@ .small-6.columns = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, {include_blank: false}, - "ng-model" => "order.bill_address.state_id" + "ng-model" => "order.bill_address.state_id", required: true .row .small-6.columns = validated_input "Postcode", "order.bill_address.zipcode" From a4be0ff55a2ef4cb7dd78412ca6a5018db6ab167 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 27 Aug 2014 17:07:30 +1000 Subject: [PATCH 180/205] Comment out ERPs which have confusing names, use old 'permits' --- app/views/admin/enterprise_relationships/_form.html.haml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index abb96f3c8f..b9c2f265cd 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -2,10 +2,11 @@ %td %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} %td - %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} - %label - %input{type: "checkbox", "ng-model" => "permissions[permission]"} - {{ EnterpriseRelationships.permission_presentation(permission) }} + permits + / %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} + / %label + / %input{type: "checkbox", "ng-model" => "permissions[permission]"} + / {{ EnterpriseRelationships.permission_presentation(permission) }} %td %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} %td.actions From 5e8bdce67df7bcc9a1307df481fde4d4157326ce Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 09:10:39 +1000 Subject: [PATCH 181/205] Refactor spec --- app/helpers/order_cycles_helper.rb | 2 +- spec/features/admin/order_cycles_spec.rb | 28 ++++++++++-------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 92cd968e0b..3fc36bfc4d 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -4,7 +4,7 @@ module OrderCyclesHelper end def coordinating_enterprises - Enterprise.is_distributor.managed_by(spree_current_user).order('name') + Enterprise.is_distributor.managed_by(spree_current_user).by_name end def order_cycle_local_remote_class(distributor, order_cycle) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 2423a6dc05..d8adbd0226 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -436,12 +436,13 @@ feature %q{ context "as an enterprise user" do - let(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } - let(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } - let(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } - let(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } + let!(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } + let!(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } + let!(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } + let!(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } let!(:distributor1_fee) { create(:enterprise_fee, enterprise: distributor1, name: 'First Distributor Fee') } - before(:each) do + + before do product = create(:product, supplier: supplier1) product.distributors << distributor1 product.save! @@ -486,17 +487,12 @@ feature %q{ select 'First Distributor', from: 'new_distributor_id' click_button 'Add distributor' - # Should only have suppliers / distributors listed which the user can manage - within "#new_supplier_id" do - page.should_not have_content supplier2.name - end - within "#new_distributor_id" do - page.should_not have_content distributor2.name - end - within "#order_cycle_coordinator_id" do - page.should_not have_content distributor2.name - page.should_not have_content supplier1.name - page.should_not have_content supplier2.name + # Should only have suppliers / distributors listed which the user is managing + page.should_not have_select 'new_supplier_id', with_options: [supplier2.name] + page.should_not have_select 'new_distributor_id', with_options: [distributor2.name] + + [distributor2.name, supplier1.name, supplier2.name].each do |enterprise_name| + page.should_not have_select 'order_cycle_coordinator_id', with_options: [enterprise_name] end click_button 'Create' From a7689973bef9ddb1c617d43b34e7689fe5b9e702 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 09:23:48 +1000 Subject: [PATCH 182/205] Semantically name enterprises in spec --- spec/features/admin/order_cycles_spec.rb | 55 ++++++++++++------------ 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index d8adbd0226..8bbd544f16 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -436,27 +436,27 @@ feature %q{ context "as an enterprise user" do - let!(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } - let!(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } - let!(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } - let!(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } - let!(:distributor1_fee) { create(:enterprise_fee, enterprise: distributor1, name: 'First Distributor Fee') } + let!(:supplier_managed) { create(:supplier_enterprise, name: 'Managed supplier') } + let!(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Unmanaged supplier') } + let!(:distributor_managed) { create(:distributor_enterprise, name: 'Managed distributor') } + let!(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Unmanaged Distributor') } + let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor Fee') } before do - product = create(:product, supplier: supplier1) - product.distributors << distributor1 + product = create(:product, supplier: supplier_managed) + product.distributors << distributor_managed product.save! @new_user = create_enterprise_user - @new_user.enterprise_roles.build(enterprise: supplier1).save - @new_user.enterprise_roles.build(enterprise: distributor1).save + @new_user.enterprise_roles.build(enterprise: supplier_managed).save + @new_user.enterprise_roles.build(enterprise: distributor_managed).save login_to_admin_as @new_user end scenario "viewing a list of order cycles I am coordinating" do - oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) - oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier2, name: 'Order Cycle 2' } ) + oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) + oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier_unmanaged, name: 'Order Cycle 2' } ) click_link "Order Cycles" @@ -465,8 +465,8 @@ feature %q{ page.should_not have_content oc_for_other_user.name # The order cycle should not show enterprises that I don't manage - page.should_not have_selector 'td.suppliers', text: supplier2.name - page.should_not have_selector 'td.distributors', text: distributor2.name + page.should_not have_selector 'td.suppliers', text: supplier_unmanaged.name + page.should_not have_selector 'td.distributors', text: distributor_unmanaged.name end scenario "creating a new order cycle" do @@ -477,21 +477,22 @@ feature %q{ fill_in 'order_cycle_orders_open_at', with: '2012-11-06 06:00:00' fill_in 'order_cycle_orders_close_at', with: '2012-11-13 17:00:00' - select 'First Supplier', from: 'new_supplier_id' + select 'Managed supplier', from: 'new_supplier_id' click_button 'Add supplier' - select 'First Distributor', from: 'order_cycle_coordinator_id' + select 'Managed distributor', from: 'order_cycle_coordinator_id' click_button 'Add coordinator fee' - select 'First Distributor Fee', from: 'order_cycle_coordinator_fee_0_id' + select 'Managed distributor Fee', from: 'order_cycle_coordinator_fee_0_id' - select 'First Distributor', from: 'new_distributor_id' + select 'Managed distributor', from: 'new_distributor_id' click_button 'Add distributor' - # Should only have suppliers / distributors listed which the user is managing - page.should_not have_select 'new_supplier_id', with_options: [supplier2.name] - page.should_not have_select 'new_distributor_id', with_options: [distributor2.name] + # Should only have suppliers / distributors listed which the user is managing or + # has E2E permission to add products to order cycles + page.should_not have_select 'new_supplier_id', with_options: [supplier_unmanaged.name] + page.should_not have_select 'new_distributor_id', with_options: [distributor_unmanaged.name] - [distributor2.name, supplier1.name, supplier2.name].each do |enterprise_name| + [distributor_unmanaged.name, supplier_managed.name, supplier_unmanaged.name].each do |enterprise_name| page.should_not have_select 'order_cycle_coordinator_id', with_options: [enterprise_name] end @@ -499,15 +500,15 @@ feature %q{ flash_message.should == "Your order cycle has been created." order_cycle = OrderCycle.find_by_name('My order cycle') - order_cycle.coordinator.should == distributor1 + order_cycle.coordinator.should == distributor_managed end scenario "editing an order cycle" do - oc = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) visit edit_admin_order_cycle_path(oc) - # I should not see exchanges for supplier2 or distributor2 + # I should not see exchanges for supplier_unmanaged or distributor_unmanaged page.all('tr.supplier').count.should == 1 page.all('tr.distributor').count.should == 1 @@ -516,9 +517,9 @@ feature %q{ page.should have_content "Your order cycle has been updated." oc.reload - oc.suppliers.sort.should == [supplier1, supplier2] - oc.coordinator.should == supplier1 - oc.distributors.sort.should == [distributor1, distributor2] + oc.suppliers.sort.should == [supplier_managed, supplier_unmanaged] + oc.coordinator.should == supplier_managed + oc.distributors.sort.should == [distributor_managed, distributor_unmanaged] end scenario "cloning an order cycle" do From 8548a1a67e0531ca29bf660338b23f088ebaf947 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 09:51:18 +1000 Subject: [PATCH 183/205] Determine producer options on order cycle screen through OpenFoodNetwork::Permissions class --- .../admin/order_cycles_controller.rb | 1 + app/views/admin/order_cycles/_form.html.haml | 2 +- lib/open_food_network/permissions.rb | 19 +++++++++++++++++++ .../lib/open_food_network/permissions_spec.rb | 16 ++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 lib/open_food_network/permissions.rb create mode 100644 spec/lib/open_food_network/permissions_spec.rb diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 566c660112..58c04a63bf 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -1,3 +1,4 @@ +require 'open_food_network/permissions' require 'open_food_network/order_cycle_form_applicator' module Admin diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index 68dfed9f1a..4087ebcfd5 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -25,7 +25,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_supplied_products_form' -= select_tag :new_supplier_id, options_from_collection_for_select(Enterprise.is_primary_producer.managed_by(spree_current_user).by_name, :id, :name), {'ng-model' => 'new_supplier_id'} += select_tag :new_supplier_id, options_from_collection_for_select(OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_producers, :id, :name), {'ng-model' => 'new_supplier_id'} = f.submit 'Add supplier', 'ng-click' => 'addSupplier($event)' diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb new file mode 100644 index 0000000000..80ceba55ee --- /dev/null +++ b/lib/open_food_network/permissions.rb @@ -0,0 +1,19 @@ +module OpenFoodNetwork + class Permissions + def initialize(user) + @user = user + end + + def order_cycle_producers + managed_producers + end + + + private + + def managed_producers + Enterprise.managed_by(@user).is_primary_producer.by_name + end + + end +end diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb new file mode 100644 index 0000000000..0c0449c145 --- /dev/null +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -0,0 +1,16 @@ +require 'open_food_network/permissions' + +module OpenFoodNetwork + describe Permissions do + let(:user) { double(:user) } + let(:permissions) { Permissions.new(user) } + let(:producer) { double(:enterprise) } + + describe "finding producers that can be added to an order cycle" do + it "returns managed producers" do + permissions.stub(:managed_producers) { [producer] } + permissions.order_cycle_producers.should == [producer] + end + end + end +end From b9e582149785b39a571262641d2c2e34608536d8 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 10:32:31 +1000 Subject: [PATCH 184/205] Add EnterpriseRelationship scopes permitting and with_permission --- app/models/enterprise_relationship.rb | 10 +++++++++- spec/models/enterprise_relationship_spec.rb | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 5c10aafbbf..8e2f3bcf06 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -9,12 +9,20 @@ class EnterpriseRelationship < ActiveRecord::Base scope :with_enterprises, joins('LEFT JOIN enterprises AS parent_enterprises ON parent_enterprises.id = enterprise_relationships.parent_id'). joins('LEFT JOIN enterprises AS child_enterprises ON child_enterprises.id = enterprise_relationships.child_id') - scope :by_name, with_enterprises.order('parent_enterprises.name, child_enterprises.name') scope :involving_enterprises, ->(enterprises) { where('parent_id IN (?) OR child_id IN (?)', enterprises, enterprises) } + scope :permitting, ->(enterprises) { where('child_id IN (?)', enterprises) } + + scope :with_permission, ->(permission) { + joins(:permissions). + where('enterprise_relationship_permissions.name = ?', permission) + } + + scope :by_name, with_enterprises.order('parent_enterprises.name, child_enterprises.name') + def permissions_list=(perms) perms.andand.each { |name| permissions.build name: name } diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index 6cdf3db657..3201e1db5c 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -43,5 +43,24 @@ describe EnterpriseRelationship do er.permissions.should be_empty end end + + it "finds relationships that grant permissions to some enterprises" do + er1 = create(:enterprise_relationship, parent: e2, child: e1) + er2 = create(:enterprise_relationship, parent: e3, child: e2) + er3 = create(:enterprise_relationship, parent: e1, child: e3) + + EnterpriseRelationship.permitting([e1, e2]).sort.should == [er1, er2] + end + + it "finds relationships that grant a particular permission" do + er1 = create(:enterprise_relationship, parent: e1, child: e2, + permissions_list: ['one', 'two']) + er2 = create(:enterprise_relationship, parent: e2, child: e3, + permissions_list: ['two', 'three']) + er3 = create(:enterprise_relationship, parent: e3, child: e1, + permissions_list: ['three', 'four']) + + EnterpriseRelationship.with_permission('two').sort.should == [er1, er2].sort + end end end From 34602244caa529b627242075af18f561652b5d3b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 11:03:16 +1000 Subject: [PATCH 185/205] Show permitted suppliers in order cycle add supplier select box --- lib/open_food_network/permissions.rb | 23 ++++++++++-- spec/features/admin/order_cycles_spec.rb | 11 +++++- .../lib/open_food_network/permissions_spec.rb | 37 +++++++++++++++++-- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 80ceba55ee..a961a3f779 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -5,15 +5,32 @@ module OpenFoodNetwork end def order_cycle_producers - managed_producers + (managed_producers + related_producers_with(:add_products_to_order_cycle)). + sort_by(&:name) end private - def managed_producers - Enterprise.managed_by(@user).is_primary_producer.by_name + def managed_enterprises + Enterprise.managed_by(@user) end + def managed_producers + managed_enterprises.is_primary_producer.by_name + end + + def related_enterprises_with(permission) + parent_ids = EnterpriseRelationship. + permitting(managed_enterprises). + with_permission(permission). + pluck(:parent_id) + + Enterprise.where('id IN (?)', parent_ids) + end + + def related_producers_with(permission) + related_enterprises_with(permission).is_primary_producer + end end end diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 8bbd544f16..9aad54f3d6 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -438,9 +438,14 @@ feature %q{ let!(:supplier_managed) { create(:supplier_enterprise, name: 'Managed supplier') } let!(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Unmanaged supplier') } + let!(:supplier_permitted) { create(:supplier_enterprise, name: 'Permitted supplier') } let!(:distributor_managed) { create(:distributor_enterprise, name: 'Managed distributor') } let!(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Unmanaged Distributor') } - let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor Fee') } + let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor fee') } + let!(:supplier_permitted_relationship) do + create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed, + permissions_list: [:add_products_to_order_cycle]) + end before do product = create(:product, supplier: supplier_managed) @@ -479,10 +484,12 @@ feature %q{ select 'Managed supplier', from: 'new_supplier_id' click_button 'Add supplier' + select 'Permitted supplier', from: 'new_supplier_id' + click_button 'Add supplier' select 'Managed distributor', from: 'order_cycle_coordinator_id' click_button 'Add coordinator fee' - select 'Managed distributor Fee', from: 'order_cycle_coordinator_fee_0_id' + select 'Managed distributor fee', from: 'order_cycle_coordinator_fee_0_id' select 'Managed distributor', from: 'new_distributor_id' click_button 'Add distributor' diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index 0c0449c145..573a6e91f3 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -4,12 +4,41 @@ module OpenFoodNetwork describe Permissions do let(:user) { double(:user) } let(:permissions) { Permissions.new(user) } - let(:producer) { double(:enterprise) } + let(:permission) { 'one' } + let(:e1) { create(:enterprise) } + let(:e2) { create(:enterprise) } describe "finding producers that can be added to an order cycle" do - it "returns managed producers" do - permissions.stub(:managed_producers) { [producer] } - permissions.order_cycle_producers.should == [producer] + let(:producer1) { double(:enterprise1, name: 'A') } + let(:producer2) { double(:enterprise2, name: 'B') } + let(:producer3) { double(:enterprise3, name: 'C') } + + it "returns managed producers and related+permitted enterprises, sorted by name" do + permissions.stub(:managed_producers) { [producer1, producer3] } + permissions.stub(:related_producers_with) { [producer2] } + + permissions.order_cycle_producers.should == [producer1, producer2, producer3] + end + end + + describe "finding related enterprises with a particular permission" do + let!(:er) { create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [permission]) } + + it "returns the enterprises" do + permissions.stub(:managed_enterprises) { e2 } + permissions.send(:related_enterprises_with, permission).should == [e1] + end + + it "returns an empty array when there are none" do + permissions.stub(:managed_enterprises) { e1 } + permissions.send(:related_enterprises_with, permission).should == [] + end + end + + describe "finding related producers with a particular permission" do + it "returns permitted related enterprises that are also producers" do + permissions.stub_chain(:related_enterprises_with, :is_primary_producer) { [e1] } + permissions.send(:related_producers_with, permission).should == [e1] end end end From 099a5b0b7b32455f3441342c14cbd9d3b8b3af71 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 11:38:31 +1000 Subject: [PATCH 186/205] Show E2E related enterprise exchanges in OC --- app/views/admin/order_cycles/show.rep | 2 +- lib/open_food_network/permissions.rb | 7 ++++ spec/features/admin/order_cycles_spec.rb | 10 +++--- .../lib/open_food_network/permissions_spec.rb | 32 +++++++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/app/views/admin/order_cycles/show.rep b/app/views/admin/order_cycles/show.rep index 04fc659813..fb71602eb6 100644 --- a/app/views/admin/order_cycles/show.rep +++ b/app/views/admin/order_cycles/show.rep @@ -9,7 +9,7 @@ r.element :order_cycle, @order_cycle do r.element :id end - r.list_of :exchanges, @order_cycle.exchanges.managed_by(spree_current_user).order('id ASC') do |exchange| + r.list_of :exchanges, OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_exchanges(@order_cycle).order('id ASC') do |exchange| r.element :id r.element :sender_id r.element :receiver_id diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index a961a3f779..55ec1b74a8 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -4,11 +4,18 @@ module OpenFoodNetwork @user = user end + # Find producers for which an admin is allowed to add their products to an order cycle def order_cycle_producers (managed_producers + related_producers_with(:add_products_to_order_cycle)). sort_by(&:name) end + # Find the exchanges of an order cycle that an admin can manage + def order_cycle_exchanges(order_cycle) + enterprises = managed_enterprises + related_enterprises_with(:add_products_to_order_cycle) + order_cycle.exchanges.to_enterprises(enterprises).from_enterprises(enterprises) + end + private diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 9aad54f3d6..26488fd9f4 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -511,12 +511,12 @@ feature %q{ end scenario "editing an order cycle" do - oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) visit edit_admin_order_cycle_path(oc) # I should not see exchanges for supplier_unmanaged or distributor_unmanaged - page.all('tr.supplier').count.should == 1 + page.all('tr.supplier').count.should == 2 page.all('tr.distributor').count.should == 1 # When I save, then those exchanges should remain @@ -524,13 +524,13 @@ feature %q{ page.should have_content "Your order cycle has been updated." oc.reload - oc.suppliers.sort.should == [supplier_managed, supplier_unmanaged] + oc.suppliers.sort.should == [supplier_managed, supplier_permitted, supplier_unmanaged].sort oc.coordinator.should == supplier_managed - oc.distributors.sort.should == [distributor_managed, distributor_unmanaged] + oc.distributors.sort.should == [distributor_managed, distributor_unmanaged].sort end scenario "cloning an order cycle" do - oc = create(:simple_order_cycle) + oc = create(:simple_order_cycle, coordinator: distributor_managed) click_link "Order Cycles" first('a.clone-order-cycle').click diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index 573a6e91f3..f44e9daa89 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -21,6 +21,38 @@ module OpenFoodNetwork end end + describe "finding exchanges of an order cycle that an admin can manage" do + let(:oc) { create(:simple_order_cycle) } + let!(:ex) { create(:exchange, order_cycle: oc, sender: e1, receiver: e2) } + + before do + permissions.stub(:managed_enterprises) { [] } + permissions.stub(:related_enterprises_with) { [] } + end + + it "returns exchanges involving enterprises managed by the user" do + permissions.stub(:managed_enterprises) { [e1, e2] } + permissions.order_cycle_exchanges(oc).should == [ex] + end + + it "returns exchanges involving enterprises with E2E permission" do + permissions.stub(:related_enterprises_with) { [e1, e2] } + permissions.order_cycle_exchanges(oc).should == [ex] + end + + it "does not return exchanges involving only the sender" do + permissions.stub(:managed_enterprises) { [e1] } + permissions.order_cycle_exchanges(oc).should == [] + end + + it "does not return exchanges involving only the receiver" do + permissions.stub(:managed_enterprises) { [e2] } + permissions.order_cycle_exchanges(oc).should == [] + end + end + + ######################################## + describe "finding related enterprises with a particular permission" do let!(:er) { create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [permission]) } From 5ef13d3c5aaeb5d91249a17c260436b4ea0faabe Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 14:46:23 +1000 Subject: [PATCH 187/205] Change 'add products to OC' permission into the more general 'add enterprise to OC' --- .../enterprise_relationships.js.coffee | 6 ++--- app/helpers/order_cycles_helper.rb | 4 +++ app/views/admin/order_cycles/_form.html.haml | 2 +- lib/open_food_network/permissions.rb | 20 +++++--------- .../admin/enterprise_relationships_spec.rb | 14 +++++----- spec/features/admin/order_cycles_spec.rb | 2 +- .../enterprise_relationships_spec.js.coffee | 2 +- .../lib/open_food_network/permissions_spec.rb | 27 +++++++++---------- 8 files changed, 36 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index 7a799f2f28..cad556efd8 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -2,7 +2,7 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris new class EnterpriseRelationships create_errors: "" all_permissions: [ - 'add_products_to_order_cycle' + 'add_to_order_cycle' 'manage_products' ] @@ -24,5 +24,5 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris permission_presentation: (permission) -> switch permission - when "add_products_to_order_cycle" then "can add products to order cycle from" - when "manage_products" then "can manage the products of" \ No newline at end of file + when "add_to_order_cycle" then "can add to order cycle" + when "manage_products" then "can manage the products of" diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 3fc36bfc4d..e8e51815a6 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -3,6 +3,10 @@ module OrderCyclesHelper @current_order_cycle ||= current_order(false).andand.order_cycle end + def order_cycle_producer_enterprises + OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises.is_primary_producer.by_name + end + def coordinating_enterprises Enterprise.is_distributor.managed_by(spree_current_user).by_name end diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index 4087ebcfd5..aac9f07d6b 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -25,7 +25,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_supplied_products_form' -= select_tag :new_supplier_id, options_from_collection_for_select(OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_producers, :id, :name), {'ng-model' => 'new_supplier_id'} += select_tag :new_supplier_id, options_from_collection_for_select(order_cycle_producer_enterprises, :id, :name), {'ng-model' => 'new_supplier_id'} = f.submit 'Add supplier', 'ng-click' => 'addSupplier($event)' diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 55ec1b74a8..99c0aa0c15 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -4,15 +4,17 @@ module OpenFoodNetwork @user = user end - # Find producers for which an admin is allowed to add their products to an order cycle - def order_cycle_producers - (managed_producers + related_producers_with(:add_products_to_order_cycle)). - sort_by(&:name) + # Find enterprises that an admin is allowed to add to an order cycle + def order_cycle_enterprises + managed_enterprise_ids = managed_enterprises.pluck :id + permitted_enterprise_ids = related_enterprises_with(:add_to_order_cycle).pluck :id + + Enterprise.where('id IN (?)', managed_enterprise_ids + permitted_enterprise_ids) end # Find the exchanges of an order cycle that an admin can manage def order_cycle_exchanges(order_cycle) - enterprises = managed_enterprises + related_enterprises_with(:add_products_to_order_cycle) + enterprises = managed_enterprises + related_enterprises_with(:add_to_order_cycle) order_cycle.exchanges.to_enterprises(enterprises).from_enterprises(enterprises) end @@ -23,10 +25,6 @@ module OpenFoodNetwork Enterprise.managed_by(@user) end - def managed_producers - managed_enterprises.is_primary_producer.by_name - end - def related_enterprises_with(permission) parent_ids = EnterpriseRelationship. permitting(managed_enterprises). @@ -35,9 +33,5 @@ module OpenFoodNetwork Enterprise.where('id IN (?)', parent_ids) end - - def related_producers_with(permission) - related_enterprises_with(permission).is_primary_producer - end end end diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 20c7b28ac2..ba7dbb2fec 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -14,9 +14,9 @@ feature %q{ scenario "listing relationships" do # Given some enterprises with relationships e1, e2, e3, e4 = create(:enterprise), create(:enterprise), create(:enterprise), create(:enterprise) - create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:add_products_to_order_cycle]) + create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:add_to_order_cycle]) create(:enterprise_relationship, parent: e2, child: e3, permissions_list: [:manage_products]) - create(:enterprise_relationship, parent: e3, child: e4, permissions_list: [:add_products_to_order_cycle, :manage_products]) + create(:enterprise_relationship, parent: e3, child: e4, permissions_list: [:add_to_order_cycle, :manage_products]) # When I go to the relationships page click_link 'Enterprises' @@ -24,10 +24,10 @@ feature %q{ # Then I should see the relationships within('table#enterprise-relationships') do - page.should have_relationship e1, e2, ['can add products to order cycle from'] + page.should have_relationship e1, e2, ['can add to order cycle'] page.should have_relationship e2, e3, ['can manage the products of'] page.should have_relationship e3, e4, - ['can add products to order cycle from', 'can manage the products of'] + ['can add to order cycle', 'can manage the products of'] end end @@ -38,16 +38,16 @@ feature %q{ visit admin_enterprise_relationships_path select 'One', from: 'enterprise_relationship_parent_id' - check 'can add products to order cycle from' + check 'can add to order cycle' check 'can manage the products of' uncheck 'can manage the products of' select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2, ['can add products to order cycle from'] + page.should have_relationship e1, e2, ['can add to order cycle'] er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first er.should be_present - er.permissions.map(&:name).should == ['add_products_to_order_cycle'] + er.permissions.map(&:name).should == ['add_to_order_cycle'] end diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 26488fd9f4..9dee3ebd87 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -444,7 +444,7 @@ feature %q{ let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor fee') } let!(:supplier_permitted_relationship) do create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed, - permissions_list: [:add_products_to_order_cycle]) + permissions_list: [:add_to_order_cycle]) end before do diff --git a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee index 4e90f65335..9701377a73 100644 --- a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -12,5 +12,5 @@ describe "enterprise relationships", -> EnterpriseRelationships = _EnterpriseRelationships_ it "presents permission names", -> - expect(EnterpriseRelationships.permission_presentation("add_products_to_order_cycle")).toEqual "can add products to order cycle from" + expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "can add to order cycle" expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "can manage the products of" diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index f44e9daa89..b0a004cbf1 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -8,16 +8,20 @@ module OpenFoodNetwork let(:e1) { create(:enterprise) } let(:e2) { create(:enterprise) } - describe "finding producers that can be added to an order cycle" do - let(:producer1) { double(:enterprise1, name: 'A') } - let(:producer2) { double(:enterprise2, name: 'B') } - let(:producer3) { double(:enterprise3, name: 'C') } + describe "finding enterprises that can be added to an order cycle" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where('1=0') } + permissions.stub(:related_enterprises_with) { Enterprise.where('1=0') } + end - it "returns managed producers and related+permitted enterprises, sorted by name" do - permissions.stub(:managed_producers) { [producer1, producer3] } - permissions.stub(:related_producers_with) { [producer2] } + it "returns managed enterprises" do + permissions.stub(:managed_enterprises) { Enterprise.where(id: e1) } + permissions.order_cycle_enterprises.should == [e1] + end - permissions.order_cycle_producers.should == [producer1, producer2, producer3] + it "returns permitted enterprises" do + permissions.stub(:related_enterprises_with) { Enterprise.where(id: e2) } + permissions.order_cycle_enterprises.should == [e2] end end @@ -66,12 +70,5 @@ module OpenFoodNetwork permissions.send(:related_enterprises_with, permission).should == [] end end - - describe "finding related producers with a particular permission" do - it "returns permitted related enterprises that are also producers" do - permissions.stub_chain(:related_enterprises_with, :is_primary_producer) { [e1] } - permissions.send(:related_producers_with, permission).should == [e1] - end - end end end From 628d87b69a18249805ddbd1a2e45bed18385c985 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 15:00:13 +1000 Subject: [PATCH 188/205] Add to OC permission allows adding distributors to order cycle --- app/helpers/order_cycles_helper.rb | 6 +++++- app/views/admin/order_cycles/_form.html.haml | 2 +- spec/features/admin/order_cycles_spec.rb | 13 ++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index e8e51815a6..4dc13be4fa 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -8,7 +8,11 @@ module OrderCyclesHelper end def coordinating_enterprises - Enterprise.is_distributor.managed_by(spree_current_user).by_name + order_cycle_hub_enterprises + end + + def order_cycle_hub_enterprises + OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises.is_distributor.by_name end def order_cycle_local_remote_class(distributor, order_cycle) diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index aac9f07d6b..e859374a3b 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -50,7 +50,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_distributed_products_form' -= select_tag :new_distributor_id, options_from_collection_for_select(Enterprise.is_distributor.managed_by(spree_current_user).by_name, :id, :name), {'ng-model' => 'new_distributor_id'} += select_tag :new_distributor_id, options_from_collection_for_select(order_cycle_hub_enterprises, :id, :name), {'ng-model' => 'new_distributor_id'} = f.submit 'Add distributor', 'ng-click' => 'addDistributor($event)' .actions diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 9dee3ebd87..368eb0b82a 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -441,11 +441,16 @@ feature %q{ let!(:supplier_permitted) { create(:supplier_enterprise, name: 'Permitted supplier') } let!(:distributor_managed) { create(:distributor_enterprise, name: 'Managed distributor') } let!(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Unmanaged Distributor') } + let!(:distributor_permitted) { create(:distributor_enterprise, name: 'Permitted distributor') } let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor fee') } let!(:supplier_permitted_relationship) do create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed, permissions_list: [:add_to_order_cycle]) end + let!(:distributor_permitted_relationship) do + create(:enterprise_relationship, parent: distributor_permitted, child: distributor_managed, + permissions_list: [:add_to_order_cycle]) + end before do product = create(:product, supplier: supplier_managed) @@ -493,6 +498,8 @@ feature %q{ select 'Managed distributor', from: 'new_distributor_id' click_button 'Add distributor' + select 'Permitted distributor', from: 'new_distributor_id' + click_button 'Add distributor' # Should only have suppliers / distributors listed which the user is managing or # has E2E permission to add products to order cycles @@ -511,13 +518,13 @@ feature %q{ end scenario "editing an order cycle" do - oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) visit edit_admin_order_cycle_path(oc) # I should not see exchanges for supplier_unmanaged or distributor_unmanaged page.all('tr.supplier').count.should == 2 - page.all('tr.distributor').count.should == 1 + page.all('tr.distributor').count.should == 2 # When I save, then those exchanges should remain click_button 'Update' @@ -526,7 +533,7 @@ feature %q{ oc.reload oc.suppliers.sort.should == [supplier_managed, supplier_permitted, supplier_unmanaged].sort oc.coordinator.should == supplier_managed - oc.distributors.sort.should == [distributor_managed, distributor_unmanaged].sort + oc.distributors.sort.should == [distributor_managed, distributor_permitted, distributor_unmanaged].sort end scenario "cloning an order cycle" do From a5debc19dce91abeaebcd7de4bc74f119124b4a1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 15:25:17 +1000 Subject: [PATCH 189/205] Permit edits to exchanges involving enterprises permitted via E2E relationships --- .../admin/order_cycles_controller.rb | 6 +++-- app/helpers/order_cycles_helper.rb | 8 ++++-- .../order_cycles/_exchange_form.html.haml | 2 +- spec/features/admin/order_cycles_spec.rb | 26 ++++++++++++++++++- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 58c04a63bf..63daf918de 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -3,6 +3,8 @@ require 'open_food_network/order_cycle_form_applicator' module Admin class OrderCyclesController < ResourceController + include OrderCyclesHelper + before_filter :load_order_cycle_set, :only => :index def show @@ -24,7 +26,7 @@ module Admin respond_to do |format| if @order_cycle.save - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! flash[:notice] = 'Your order cycle has been created.' format.html { redirect_to admin_order_cycles_path } @@ -41,7 +43,7 @@ module Admin respond_to do |format| if @order_cycle.update_attributes(params[:order_cycle]) - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! flash[:notice] = 'Your order cycle has been updated.' format.html { redirect_to admin_order_cycles_path } diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 4dc13be4fa..dfd957a143 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -3,8 +3,12 @@ module OrderCyclesHelper @current_order_cycle ||= current_order(false).andand.order_cycle end + def order_cycle_permitted_enterprises + OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises + end + def order_cycle_producer_enterprises - OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises.is_primary_producer.by_name + order_cycle_permitted_enterprises.is_primary_producer.by_name end def coordinating_enterprises @@ -12,7 +16,7 @@ module OrderCyclesHelper end def order_cycle_hub_enterprises - OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises.is_distributor.by_name + order_cycle_permitted_enterprises.is_distributor.by_name end def order_cycle_local_remote_class(distributor, order_cycle) diff --git a/app/views/admin/order_cycles/_exchange_form.html.haml b/app/views/admin/order_cycles/_exchange_form.html.haml index 3539892034..8f81b9f03f 100644 --- a/app/views/admin/order_cycles/_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_form.html.haml @@ -23,4 +23,4 @@ = f.submit 'Add fee', 'ng-click' => 'addExchangeFee($event, exchange)' %td.actions - %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text"} + %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text remove-exchange"} diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 368eb0b82a..76ee3bdfc2 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -514,10 +514,12 @@ feature %q{ flash_message.should == "Your order cycle has been created." order_cycle = OrderCycle.find_by_name('My order cycle') + order_cycle.suppliers.sort.should == [supplier_managed, supplier_permitted].sort order_cycle.coordinator.should == distributor_managed + order_cycle.distributors.sort.should == [distributor_managed, distributor_permitted].sort end - scenario "editing an order cycle" do + scenario "editing an order cycle does not affect exchanges we don't manage" do oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) visit edit_admin_order_cycle_path(oc) @@ -536,6 +538,28 @@ feature %q{ oc.distributors.sort.should == [distributor_managed, distributor_permitted, distributor_unmanaged].sort end + scenario "editing an order cycle" do + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + + visit edit_admin_order_cycle_path(oc) + + # When I remove all the exchanges and save + page.find("tr.supplier-#{supplier_managed.id} a.remove-exchange").click + page.find("tr.supplier-#{supplier_permitted.id} a.remove-exchange").click + page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click + page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click + click_button 'Update' + + # Then the exchanges should be removed + page.should have_content "Your order cycle has been updated." + + oc.reload + oc.suppliers.should == [supplier_unmanaged] + oc.coordinator.should == supplier_managed + oc.distributors.should == [distributor_unmanaged] + end + + scenario "cloning an order cycle" do oc = create(:simple_order_cycle, coordinator: distributor_managed) From 1871d42e68e37a3bd4414743e6febdefa8d95aae Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 26 Aug 2014 15:42:35 +1000 Subject: [PATCH 190/205] Switch to correct grammatical ordering of child/parent enterprise on enterprise relationships page --- app/models/enterprise_relationship.rb | 2 +- .../_enterprise_relationship.html.haml | 4 ++-- app/views/admin/enterprise_relationships/_form.html.haml | 4 ++-- spec/features/admin/enterprise_relationships_spec.rb | 2 +- spec/models/enterprise_relationship_spec.rb | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 8e2f3bcf06..dba99cc7b3 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -21,7 +21,7 @@ class EnterpriseRelationship < ActiveRecord::Base where('enterprise_relationship_permissions.name = ?', permission) } - scope :by_name, with_enterprises.order('parent_enterprises.name, child_enterprises.name') + scope :by_name, with_enterprises.order('child_enterprises.name, parent_enterprises.name') def permissions_list=(perms) diff --git a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml index 56f0d46c01..6a7dcdc7f4 100644 --- a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml +++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml @@ -1,8 +1,8 @@ -%td {{ enterprise_relationship.parent_name }} +%td {{ enterprise_relationship.child_name }} %td %ul %li{"ng-repeat" => "permission in enterprise_relationship.permissions"} {{ EnterpriseRelationships.permission_presentation(permission.name) }} -%td {{ enterprise_relationship.child_name }} +%td {{ enterprise_relationship.parent_name }} %td.actions %a.delete-enterprise-relationship.icon-trash.no-text{'ng-click' => 'delete(enterprise_relationship)'} diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index b9c2f265cd..2fe9402d3a 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -1,6 +1,6 @@ %tr %td - %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} + %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} %td permits / %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} @@ -8,7 +8,7 @@ / %input{type: "checkbox", "ng-model" => "permissions[permission]"} / {{ EnterpriseRelationships.permission_presentation(permission) }} %td - %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} + %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} %td.actions %input{type: "button", value: "Create", "ng-click" => "create()"} .errors {{ EnterpriseRelationships.create_errors }} diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index ba7dbb2fec..0dadb25848 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -119,6 +119,6 @@ feature %q{ def have_relationship(parent, child, perms=[]) perms = perms.join(' ') || 'permits' - have_table_row [parent.name, perms, child.name, ''] + have_table_row [child.name, perms, parent.name, ''] end end diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index 3201e1db5c..3a6c48e891 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -6,10 +6,10 @@ describe EnterpriseRelationship do let(:e2) { create(:enterprise, name: 'B') } let(:e3) { create(:enterprise, name: 'C') } - it "sorts by parent, child enterprise name" do - er1 = create(:enterprise_relationship, parent: e1, child: e3) - er2 = create(:enterprise_relationship, parent: e2, child: e1) - er3 = create(:enterprise_relationship, parent: e1, child: e2) + it "sorts by child, parent enterprise name" do + er1 = create(:enterprise_relationship, parent: e3, child: e1) + er2 = create(:enterprise_relationship, parent: e1, child: e2) + er3 = create(:enterprise_relationship, parent: e2, child: e1) EnterpriseRelationship.by_name.should == [er3, er1, er2] end From 7b89e6aa80753d9729350315b4ce41f4264306c9 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 28 Aug 2014 10:12:06 +1000 Subject: [PATCH 191/205] Revert "Comment out ERPs which have confusing names, use old 'permits'" This reverts commit a4be0ff55a2ef4cb7dd78412ca6a5018db6ab167. --- app/views/admin/enterprise_relationships/_form.html.haml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index 2fe9402d3a..86086c8781 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -2,11 +2,10 @@ %td %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} %td - permits - / %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} - / %label - / %input{type: "checkbox", "ng-model" => "permissions[permission]"} - / {{ EnterpriseRelationships.permission_presentation(permission) }} + %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} + %label + %input{type: "checkbox", "ng-model" => "permissions[permission]"} + {{ EnterpriseRelationships.permission_presentation(permission) }} %td %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} %td.actions From bfd9ffd84aed701b3799a02fbed60173e70b54af Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 28 Aug 2014 10:03:53 +1000 Subject: [PATCH 192/205] Adding missing data-bindings for country/state --- app/views/checkout/_billing.html.haml | 2 +- app/views/checkout/_details.html.haml | 16 ++++++++-------- app/views/checkout/_shipping.html.haml | 13 +++++++------ spec/features/consumer/shopping/checkout_spec.rb | 2 +- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index 1e7adec1ad..c052eb1b05 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -47,7 +47,7 @@ .small-6.columns.right = ba.select :country_id, available_countries.map{|c|[c.name, c.id]}, - {include_blank: false}, "ng-model" => "order.bill_address.country_id" + "ng-model" => "order.bill_address.country_id", required: true .row .small-12.columns.text-right diff --git a/app/views/checkout/_details.html.haml b/app/views/checkout/_details.html.haml index 6bb24d4682..6eb149804f 100644 --- a/app/views/checkout/_details.html.haml +++ b/app/views/checkout/_details.html.haml @@ -9,23 +9,23 @@ %i.ofn-i_051-check-big Your details - %accordion-group{"is-open" => "accordion.details", + %accordion-group{"is-open" => "accordion.details", "ng-class" => "{valid: details.$valid, open: accordion.details}"} %accordion-heading .row .small-8.medium-10.columns - %em + %em %small {{ summary() | printArray }} .small-4.medium-2.columns.text-right %span.accordion-up - %em + %em %small Hide - %i.ofn-i_053-point-up + %i.ofn-i_053-point-up %span.accordion-down - %em + %em %small Expand - %i.ofn-i_052-point-down + %i.ofn-i_052-point-down .row .small-6.columns @@ -36,10 +36,10 @@ .row .small-6.columns = validated_input 'Email', 'order.email', type: :email, "ofn-focus" => "accordion['details']" - + .small-6.columns = validated_input 'Phone', 'order.bill_address.phone' - + .row .small-12.columns.text-right %button.primary{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)"} Next diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index 8904facff4..27089527a2 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -14,16 +14,16 @@ %accordion-heading .row .small-8.medium-10.columns - %em + %em %small {{ Checkout.shippingMethod().name }} .small-4.medium-2.columns.text-right %span.accordion-up - %em + %em %small Hide - %i.ofn-i_053-point-up + %i.ofn-i_053-point-up %span.accordion-down - %em + %em %small Expand %i.ofn-i_052-point-down @@ -61,13 +61,14 @@ .small-6.columns = validated_input "City", "order.ship_address.city" .small-6.columns - = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]} + = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]}, {include_blank: false}, + "ng-model" => "order.ship_address.state_id", required: true .row .small-6.columns = validated_input "Postcode", "order.ship_address.zipcode" .small-6.columns.right = sa.select :country_id, available_countries.map{|c|[c.name, c.id]}, - {include_blank: false} + "ng-model" => "order.ship_address.country_id", required: true .row .small-6.columns = validated_input "First Name", "order.ship_address.firstname" diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 54091e854d..b26e924495 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -23,7 +23,7 @@ feature "As a consumer I want to check out my cart", js: true do end it "shows the current distributor on checkout" do - visit checkout_path + visit checkout_path page.should have_content distributor.name end From 5ede8d169faa0d95942b45d5b81229a4367432b1 Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 28 Aug 2014 14:32:57 +1000 Subject: [PATCH 193/205] Revert a4be0ff..7b89e6a for deployment --- .../enterprise_relationships.js.coffee | 6 +- .../admin/order_cycles_controller.rb | 7 +- app/helpers/order_cycles_helper.rb | 14 +-- app/models/enterprise_relationship.rb | 10 +- .../_enterprise_relationship.html.haml | 4 +- .../enterprise_relationships/_form.html.haml | 15 +-- .../order_cycles/_exchange_form.html.haml | 2 +- app/views/admin/order_cycles/_form.html.haml | 4 +- app/views/admin/order_cycles/show.rep | 2 +- lib/open_food_network/permissions.rb | 37 ------ .../admin/enterprise_relationships_spec.rb | 16 +-- spec/features/admin/order_cycles_spec.rb | 113 ++++++------------ .../enterprise_relationships_spec.js.coffee | 2 +- .../lib/open_food_network/permissions_spec.rb | 74 ------------ spec/models/enterprise_relationship_spec.rb | 27 +---- 15 files changed, 73 insertions(+), 260 deletions(-) delete mode 100644 lib/open_food_network/permissions.rb delete mode 100644 spec/lib/open_food_network/permissions_spec.rb diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index cad556efd8..7a799f2f28 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -2,7 +2,7 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris new class EnterpriseRelationships create_errors: "" all_permissions: [ - 'add_to_order_cycle' + 'add_products_to_order_cycle' 'manage_products' ] @@ -24,5 +24,5 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris permission_presentation: (permission) -> switch permission - when "add_to_order_cycle" then "can add to order cycle" - when "manage_products" then "can manage the products of" + when "add_products_to_order_cycle" then "can add products to order cycle from" + when "manage_products" then "can manage the products of" \ No newline at end of file diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 63daf918de..566c660112 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -1,10 +1,7 @@ -require 'open_food_network/permissions' require 'open_food_network/order_cycle_form_applicator' module Admin class OrderCyclesController < ResourceController - include OrderCyclesHelper - before_filter :load_order_cycle_set, :only => :index def show @@ -26,7 +23,7 @@ module Admin respond_to do |format| if @order_cycle.save - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go! flash[:notice] = 'Your order cycle has been created.' format.html { redirect_to admin_order_cycles_path } @@ -43,7 +40,7 @@ module Admin respond_to do |format| if @order_cycle.update_attributes(params[:order_cycle]) - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go! flash[:notice] = 'Your order cycle has been updated.' format.html { redirect_to admin_order_cycles_path } diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index dfd957a143..92cd968e0b 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -3,20 +3,8 @@ module OrderCyclesHelper @current_order_cycle ||= current_order(false).andand.order_cycle end - def order_cycle_permitted_enterprises - OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises - end - - def order_cycle_producer_enterprises - order_cycle_permitted_enterprises.is_primary_producer.by_name - end - def coordinating_enterprises - order_cycle_hub_enterprises - end - - def order_cycle_hub_enterprises - order_cycle_permitted_enterprises.is_distributor.by_name + Enterprise.is_distributor.managed_by(spree_current_user).order('name') end def order_cycle_local_remote_class(distributor, order_cycle) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index dba99cc7b3..5c10aafbbf 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -9,20 +9,12 @@ class EnterpriseRelationship < ActiveRecord::Base scope :with_enterprises, joins('LEFT JOIN enterprises AS parent_enterprises ON parent_enterprises.id = enterprise_relationships.parent_id'). joins('LEFT JOIN enterprises AS child_enterprises ON child_enterprises.id = enterprise_relationships.child_id') + scope :by_name, with_enterprises.order('parent_enterprises.name, child_enterprises.name') scope :involving_enterprises, ->(enterprises) { where('parent_id IN (?) OR child_id IN (?)', enterprises, enterprises) } - scope :permitting, ->(enterprises) { where('child_id IN (?)', enterprises) } - - scope :with_permission, ->(permission) { - joins(:permissions). - where('enterprise_relationship_permissions.name = ?', permission) - } - - scope :by_name, with_enterprises.order('child_enterprises.name, parent_enterprises.name') - def permissions_list=(perms) perms.andand.each { |name| permissions.build name: name } diff --git a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml index 6a7dcdc7f4..56f0d46c01 100644 --- a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml +++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml @@ -1,8 +1,8 @@ -%td {{ enterprise_relationship.child_name }} +%td {{ enterprise_relationship.parent_name }} %td %ul %li{"ng-repeat" => "permission in enterprise_relationship.permissions"} {{ EnterpriseRelationships.permission_presentation(permission.name) }} -%td {{ enterprise_relationship.parent_name }} +%td {{ enterprise_relationship.child_name }} %td.actions %a.delete-enterprise-relationship.icon-trash.no-text{'ng-click' => 'delete(enterprise_relationship)'} diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index 86086c8781..b9c2f265cd 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -1,13 +1,14 @@ %tr - %td - %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} - %td - %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} - %label - %input{type: "checkbox", "ng-model" => "permissions[permission]"} - {{ EnterpriseRelationships.permission_presentation(permission) }} %td %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} + %td + permits + / %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} + / %label + / %input{type: "checkbox", "ng-model" => "permissions[permission]"} + / {{ EnterpriseRelationships.permission_presentation(permission) }} + %td + %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} %td.actions %input{type: "button", value: "Create", "ng-click" => "create()"} .errors {{ EnterpriseRelationships.create_errors }} diff --git a/app/views/admin/order_cycles/_exchange_form.html.haml b/app/views/admin/order_cycles/_exchange_form.html.haml index 8f81b9f03f..3539892034 100644 --- a/app/views/admin/order_cycles/_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_form.html.haml @@ -23,4 +23,4 @@ = f.submit 'Add fee', 'ng-click' => 'addExchangeFee($event, exchange)' %td.actions - %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text remove-exchange"} + %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text"} diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index e859374a3b..68dfed9f1a 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -25,7 +25,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_supplied_products_form' -= select_tag :new_supplier_id, options_from_collection_for_select(order_cycle_producer_enterprises, :id, :name), {'ng-model' => 'new_supplier_id'} += select_tag :new_supplier_id, options_from_collection_for_select(Enterprise.is_primary_producer.managed_by(spree_current_user).by_name, :id, :name), {'ng-model' => 'new_supplier_id'} = f.submit 'Add supplier', 'ng-click' => 'addSupplier($event)' @@ -50,7 +50,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_distributed_products_form' -= select_tag :new_distributor_id, options_from_collection_for_select(order_cycle_hub_enterprises, :id, :name), {'ng-model' => 'new_distributor_id'} += select_tag :new_distributor_id, options_from_collection_for_select(Enterprise.is_distributor.managed_by(spree_current_user).by_name, :id, :name), {'ng-model' => 'new_distributor_id'} = f.submit 'Add distributor', 'ng-click' => 'addDistributor($event)' .actions diff --git a/app/views/admin/order_cycles/show.rep b/app/views/admin/order_cycles/show.rep index fb71602eb6..04fc659813 100644 --- a/app/views/admin/order_cycles/show.rep +++ b/app/views/admin/order_cycles/show.rep @@ -9,7 +9,7 @@ r.element :order_cycle, @order_cycle do r.element :id end - r.list_of :exchanges, OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_exchanges(@order_cycle).order('id ASC') do |exchange| + r.list_of :exchanges, @order_cycle.exchanges.managed_by(spree_current_user).order('id ASC') do |exchange| r.element :id r.element :sender_id r.element :receiver_id diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb deleted file mode 100644 index 99c0aa0c15..0000000000 --- a/lib/open_food_network/permissions.rb +++ /dev/null @@ -1,37 +0,0 @@ -module OpenFoodNetwork - class Permissions - def initialize(user) - @user = user - end - - # Find enterprises that an admin is allowed to add to an order cycle - def order_cycle_enterprises - managed_enterprise_ids = managed_enterprises.pluck :id - permitted_enterprise_ids = related_enterprises_with(:add_to_order_cycle).pluck :id - - Enterprise.where('id IN (?)', managed_enterprise_ids + permitted_enterprise_ids) - end - - # Find the exchanges of an order cycle that an admin can manage - def order_cycle_exchanges(order_cycle) - enterprises = managed_enterprises + related_enterprises_with(:add_to_order_cycle) - order_cycle.exchanges.to_enterprises(enterprises).from_enterprises(enterprises) - end - - - private - - def managed_enterprises - Enterprise.managed_by(@user) - end - - def related_enterprises_with(permission) - parent_ids = EnterpriseRelationship. - permitting(managed_enterprises). - with_permission(permission). - pluck(:parent_id) - - Enterprise.where('id IN (?)', parent_ids) - end - end -end diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 0dadb25848..20c7b28ac2 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -14,9 +14,9 @@ feature %q{ scenario "listing relationships" do # Given some enterprises with relationships e1, e2, e3, e4 = create(:enterprise), create(:enterprise), create(:enterprise), create(:enterprise) - create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:add_products_to_order_cycle]) create(:enterprise_relationship, parent: e2, child: e3, permissions_list: [:manage_products]) - create(:enterprise_relationship, parent: e3, child: e4, permissions_list: [:add_to_order_cycle, :manage_products]) + create(:enterprise_relationship, parent: e3, child: e4, permissions_list: [:add_products_to_order_cycle, :manage_products]) # When I go to the relationships page click_link 'Enterprises' @@ -24,10 +24,10 @@ feature %q{ # Then I should see the relationships within('table#enterprise-relationships') do - page.should have_relationship e1, e2, ['can add to order cycle'] + page.should have_relationship e1, e2, ['can add products to order cycle from'] page.should have_relationship e2, e3, ['can manage the products of'] page.should have_relationship e3, e4, - ['can add to order cycle', 'can manage the products of'] + ['can add products to order cycle from', 'can manage the products of'] end end @@ -38,16 +38,16 @@ feature %q{ visit admin_enterprise_relationships_path select 'One', from: 'enterprise_relationship_parent_id' - check 'can add to order cycle' + check 'can add products to order cycle from' check 'can manage the products of' uncheck 'can manage the products of' select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2, ['can add to order cycle'] + page.should have_relationship e1, e2, ['can add products to order cycle from'] er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first er.should be_present - er.permissions.map(&:name).should == ['add_to_order_cycle'] + er.permissions.map(&:name).should == ['add_products_to_order_cycle'] end @@ -119,6 +119,6 @@ feature %q{ def have_relationship(parent, child, perms=[]) perms = perms.join(' ') || 'permits' - have_table_row [child.name, perms, parent.name, ''] + have_table_row [parent.name, perms, child.name, ''] end end diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 76ee3bdfc2..2423a6dc05 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -436,37 +436,26 @@ feature %q{ context "as an enterprise user" do - let!(:supplier_managed) { create(:supplier_enterprise, name: 'Managed supplier') } - let!(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Unmanaged supplier') } - let!(:supplier_permitted) { create(:supplier_enterprise, name: 'Permitted supplier') } - let!(:distributor_managed) { create(:distributor_enterprise, name: 'Managed distributor') } - let!(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Unmanaged Distributor') } - let!(:distributor_permitted) { create(:distributor_enterprise, name: 'Permitted distributor') } - let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor fee') } - let!(:supplier_permitted_relationship) do - create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed, - permissions_list: [:add_to_order_cycle]) - end - let!(:distributor_permitted_relationship) do - create(:enterprise_relationship, parent: distributor_permitted, child: distributor_managed, - permissions_list: [:add_to_order_cycle]) - end - - before do - product = create(:product, supplier: supplier_managed) - product.distributors << distributor_managed + let(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } + let(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } + let(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } + let(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } + let!(:distributor1_fee) { create(:enterprise_fee, enterprise: distributor1, name: 'First Distributor Fee') } + before(:each) do + product = create(:product, supplier: supplier1) + product.distributors << distributor1 product.save! @new_user = create_enterprise_user - @new_user.enterprise_roles.build(enterprise: supplier_managed).save - @new_user.enterprise_roles.build(enterprise: distributor_managed).save + @new_user.enterprise_roles.build(enterprise: supplier1).save + @new_user.enterprise_roles.build(enterprise: distributor1).save login_to_admin_as @new_user end scenario "viewing a list of order cycles I am coordinating" do - oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) - oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier_unmanaged, name: 'Order Cycle 2' } ) + oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) + oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier2, name: 'Order Cycle 2' } ) click_link "Order Cycles" @@ -475,8 +464,8 @@ feature %q{ page.should_not have_content oc_for_other_user.name # The order cycle should not show enterprises that I don't manage - page.should_not have_selector 'td.suppliers', text: supplier_unmanaged.name - page.should_not have_selector 'td.distributors', text: distributor_unmanaged.name + page.should_not have_selector 'td.suppliers', text: supplier2.name + page.should_not have_selector 'td.distributors', text: distributor2.name end scenario "creating a new order cycle" do @@ -487,81 +476,57 @@ feature %q{ fill_in 'order_cycle_orders_open_at', with: '2012-11-06 06:00:00' fill_in 'order_cycle_orders_close_at', with: '2012-11-13 17:00:00' - select 'Managed supplier', from: 'new_supplier_id' - click_button 'Add supplier' - select 'Permitted supplier', from: 'new_supplier_id' + select 'First Supplier', from: 'new_supplier_id' click_button 'Add supplier' - select 'Managed distributor', from: 'order_cycle_coordinator_id' + select 'First Distributor', from: 'order_cycle_coordinator_id' click_button 'Add coordinator fee' - select 'Managed distributor fee', from: 'order_cycle_coordinator_fee_0_id' + select 'First Distributor Fee', from: 'order_cycle_coordinator_fee_0_id' - select 'Managed distributor', from: 'new_distributor_id' - click_button 'Add distributor' - select 'Permitted distributor', from: 'new_distributor_id' + select 'First Distributor', from: 'new_distributor_id' click_button 'Add distributor' - # Should only have suppliers / distributors listed which the user is managing or - # has E2E permission to add products to order cycles - page.should_not have_select 'new_supplier_id', with_options: [supplier_unmanaged.name] - page.should_not have_select 'new_distributor_id', with_options: [distributor_unmanaged.name] - - [distributor_unmanaged.name, supplier_managed.name, supplier_unmanaged.name].each do |enterprise_name| - page.should_not have_select 'order_cycle_coordinator_id', with_options: [enterprise_name] + # Should only have suppliers / distributors listed which the user can manage + within "#new_supplier_id" do + page.should_not have_content supplier2.name + end + within "#new_distributor_id" do + page.should_not have_content distributor2.name + end + within "#order_cycle_coordinator_id" do + page.should_not have_content distributor2.name + page.should_not have_content supplier1.name + page.should_not have_content supplier2.name end click_button 'Create' flash_message.should == "Your order cycle has been created." order_cycle = OrderCycle.find_by_name('My order cycle') - order_cycle.suppliers.sort.should == [supplier_managed, supplier_permitted].sort - order_cycle.coordinator.should == distributor_managed - order_cycle.distributors.sort.should == [distributor_managed, distributor_permitted].sort + order_cycle.coordinator.should == distributor1 end - scenario "editing an order cycle does not affect exchanges we don't manage" do - oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + scenario "editing an order cycle" do + oc = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) visit edit_admin_order_cycle_path(oc) - # I should not see exchanges for supplier_unmanaged or distributor_unmanaged - page.all('tr.supplier').count.should == 2 - page.all('tr.distributor').count.should == 2 + # I should not see exchanges for supplier2 or distributor2 + page.all('tr.supplier').count.should == 1 + page.all('tr.distributor').count.should == 1 # When I save, then those exchanges should remain click_button 'Update' page.should have_content "Your order cycle has been updated." oc.reload - oc.suppliers.sort.should == [supplier_managed, supplier_permitted, supplier_unmanaged].sort - oc.coordinator.should == supplier_managed - oc.distributors.sort.should == [distributor_managed, distributor_permitted, distributor_unmanaged].sort + oc.suppliers.sort.should == [supplier1, supplier2] + oc.coordinator.should == supplier1 + oc.distributors.sort.should == [distributor1, distributor2] end - scenario "editing an order cycle" do - oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) - - visit edit_admin_order_cycle_path(oc) - - # When I remove all the exchanges and save - page.find("tr.supplier-#{supplier_managed.id} a.remove-exchange").click - page.find("tr.supplier-#{supplier_permitted.id} a.remove-exchange").click - page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click - page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click - click_button 'Update' - - # Then the exchanges should be removed - page.should have_content "Your order cycle has been updated." - - oc.reload - oc.suppliers.should == [supplier_unmanaged] - oc.coordinator.should == supplier_managed - oc.distributors.should == [distributor_unmanaged] - end - - scenario "cloning an order cycle" do - oc = create(:simple_order_cycle, coordinator: distributor_managed) + oc = create(:simple_order_cycle) click_link "Order Cycles" first('a.clone-order-cycle').click diff --git a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee index 9701377a73..4e90f65335 100644 --- a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -12,5 +12,5 @@ describe "enterprise relationships", -> EnterpriseRelationships = _EnterpriseRelationships_ it "presents permission names", -> - expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "can add to order cycle" + expect(EnterpriseRelationships.permission_presentation("add_products_to_order_cycle")).toEqual "can add products to order cycle from" expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "can manage the products of" diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb deleted file mode 100644 index b0a004cbf1..0000000000 --- a/spec/lib/open_food_network/permissions_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'open_food_network/permissions' - -module OpenFoodNetwork - describe Permissions do - let(:user) { double(:user) } - let(:permissions) { Permissions.new(user) } - let(:permission) { 'one' } - let(:e1) { create(:enterprise) } - let(:e2) { create(:enterprise) } - - describe "finding enterprises that can be added to an order cycle" do - before do - permissions.stub(:managed_enterprises) { Enterprise.where('1=0') } - permissions.stub(:related_enterprises_with) { Enterprise.where('1=0') } - end - - it "returns managed enterprises" do - permissions.stub(:managed_enterprises) { Enterprise.where(id: e1) } - permissions.order_cycle_enterprises.should == [e1] - end - - it "returns permitted enterprises" do - permissions.stub(:related_enterprises_with) { Enterprise.where(id: e2) } - permissions.order_cycle_enterprises.should == [e2] - end - end - - describe "finding exchanges of an order cycle that an admin can manage" do - let(:oc) { create(:simple_order_cycle) } - let!(:ex) { create(:exchange, order_cycle: oc, sender: e1, receiver: e2) } - - before do - permissions.stub(:managed_enterprises) { [] } - permissions.stub(:related_enterprises_with) { [] } - end - - it "returns exchanges involving enterprises managed by the user" do - permissions.stub(:managed_enterprises) { [e1, e2] } - permissions.order_cycle_exchanges(oc).should == [ex] - end - - it "returns exchanges involving enterprises with E2E permission" do - permissions.stub(:related_enterprises_with) { [e1, e2] } - permissions.order_cycle_exchanges(oc).should == [ex] - end - - it "does not return exchanges involving only the sender" do - permissions.stub(:managed_enterprises) { [e1] } - permissions.order_cycle_exchanges(oc).should == [] - end - - it "does not return exchanges involving only the receiver" do - permissions.stub(:managed_enterprises) { [e2] } - permissions.order_cycle_exchanges(oc).should == [] - end - end - - ######################################## - - describe "finding related enterprises with a particular permission" do - let!(:er) { create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [permission]) } - - it "returns the enterprises" do - permissions.stub(:managed_enterprises) { e2 } - permissions.send(:related_enterprises_with, permission).should == [e1] - end - - it "returns an empty array when there are none" do - permissions.stub(:managed_enterprises) { e1 } - permissions.send(:related_enterprises_with, permission).should == [] - end - end - end -end diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index 3a6c48e891..6cdf3db657 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -6,10 +6,10 @@ describe EnterpriseRelationship do let(:e2) { create(:enterprise, name: 'B') } let(:e3) { create(:enterprise, name: 'C') } - it "sorts by child, parent enterprise name" do - er1 = create(:enterprise_relationship, parent: e3, child: e1) - er2 = create(:enterprise_relationship, parent: e1, child: e2) - er3 = create(:enterprise_relationship, parent: e2, child: e1) + it "sorts by parent, child enterprise name" do + er1 = create(:enterprise_relationship, parent: e1, child: e3) + er2 = create(:enterprise_relationship, parent: e2, child: e1) + er3 = create(:enterprise_relationship, parent: e1, child: e2) EnterpriseRelationship.by_name.should == [er3, er1, er2] end @@ -43,24 +43,5 @@ describe EnterpriseRelationship do er.permissions.should be_empty end end - - it "finds relationships that grant permissions to some enterprises" do - er1 = create(:enterprise_relationship, parent: e2, child: e1) - er2 = create(:enterprise_relationship, parent: e3, child: e2) - er3 = create(:enterprise_relationship, parent: e1, child: e3) - - EnterpriseRelationship.permitting([e1, e2]).sort.should == [er1, er2] - end - - it "finds relationships that grant a particular permission" do - er1 = create(:enterprise_relationship, parent: e1, child: e2, - permissions_list: ['one', 'two']) - er2 = create(:enterprise_relationship, parent: e2, child: e3, - permissions_list: ['two', 'three']) - er3 = create(:enterprise_relationship, parent: e3, child: e1, - permissions_list: ['three', 'four']) - - EnterpriseRelationship.with_permission('two').sort.should == [er1, er2].sort - end end end From ba3f97ca1f4a7ee0463ef95c6f83e95a01ebf74e Mon Sep 17 00:00:00 2001 From: Rob H Date: Thu, 28 Aug 2014 14:45:24 +1000 Subject: [PATCH 194/205] Fixing enterprise relationships spec --- spec/features/admin/enterprise_relationships_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 20c7b28ac2..757f8bc2e6 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -38,16 +38,16 @@ feature %q{ visit admin_enterprise_relationships_path select 'One', from: 'enterprise_relationship_parent_id' - check 'can add products to order cycle from' - check 'can manage the products of' - uncheck 'can manage the products of' + #check 'can add products to order cycle from' + #check 'can manage the products of' + #uncheck 'can manage the products of' select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2, ['can add products to order cycle from'] + page.should have_relationship e1, e2 #, ['can add products to order cycle from'] er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first er.should be_present - er.permissions.map(&:name).should == ['add_products_to_order_cycle'] + #er.permissions.map(&:name).should == ['add_products_to_order_cycle'] end From 0b61872d964d4ddebb870aa27b722f93a5d028a8 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 29 Aug 2014 16:38:10 +1000 Subject: [PATCH 195/205] Revert 5ede8d1, reinstating a4be0ff..7b89e6a --- .../enterprise_relationships.js.coffee | 6 +- .../admin/order_cycles_controller.rb | 7 +- app/helpers/order_cycles_helper.rb | 14 ++- app/models/enterprise_relationship.rb | 10 +- .../_enterprise_relationship.html.haml | 4 +- .../enterprise_relationships/_form.html.haml | 15 ++- .../order_cycles/_exchange_form.html.haml | 2 +- app/views/admin/order_cycles/_form.html.haml | 4 +- app/views/admin/order_cycles/show.rep | 2 +- lib/open_food_network/permissions.rb | 37 ++++++ .../admin/enterprise_relationships_spec.rb | 21 ++-- spec/features/admin/order_cycles_spec.rb | 113 ++++++++++++------ .../enterprise_relationships_spec.js.coffee | 2 +- .../lib/open_food_network/permissions_spec.rb | 74 ++++++++++++ spec/models/enterprise_relationship_spec.rb | 27 ++++- 15 files changed, 263 insertions(+), 75 deletions(-) create mode 100644 lib/open_food_network/permissions.rb create mode 100644 spec/lib/open_food_network/permissions_spec.rb diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index 7a799f2f28..cad556efd8 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -2,7 +2,7 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris new class EnterpriseRelationships create_errors: "" all_permissions: [ - 'add_products_to_order_cycle' + 'add_to_order_cycle' 'manage_products' ] @@ -24,5 +24,5 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris permission_presentation: (permission) -> switch permission - when "add_products_to_order_cycle" then "can add products to order cycle from" - when "manage_products" then "can manage the products of" \ No newline at end of file + when "add_to_order_cycle" then "can add to order cycle" + when "manage_products" then "can manage the products of" diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 566c660112..63daf918de 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -1,7 +1,10 @@ +require 'open_food_network/permissions' require 'open_food_network/order_cycle_form_applicator' module Admin class OrderCyclesController < ResourceController + include OrderCyclesHelper + before_filter :load_order_cycle_set, :only => :index def show @@ -23,7 +26,7 @@ module Admin respond_to do |format| if @order_cycle.save - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! flash[:notice] = 'Your order cycle has been created.' format.html { redirect_to admin_order_cycles_path } @@ -40,7 +43,7 @@ module Admin respond_to do |format| if @order_cycle.update_attributes(params[:order_cycle]) - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! flash[:notice] = 'Your order cycle has been updated.' format.html { redirect_to admin_order_cycles_path } diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 92cd968e0b..dfd957a143 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -3,8 +3,20 @@ module OrderCyclesHelper @current_order_cycle ||= current_order(false).andand.order_cycle end + def order_cycle_permitted_enterprises + OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises + end + + def order_cycle_producer_enterprises + order_cycle_permitted_enterprises.is_primary_producer.by_name + end + def coordinating_enterprises - Enterprise.is_distributor.managed_by(spree_current_user).order('name') + order_cycle_hub_enterprises + end + + def order_cycle_hub_enterprises + order_cycle_permitted_enterprises.is_distributor.by_name end def order_cycle_local_remote_class(distributor, order_cycle) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 5c10aafbbf..dba99cc7b3 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -9,12 +9,20 @@ class EnterpriseRelationship < ActiveRecord::Base scope :with_enterprises, joins('LEFT JOIN enterprises AS parent_enterprises ON parent_enterprises.id = enterprise_relationships.parent_id'). joins('LEFT JOIN enterprises AS child_enterprises ON child_enterprises.id = enterprise_relationships.child_id') - scope :by_name, with_enterprises.order('parent_enterprises.name, child_enterprises.name') scope :involving_enterprises, ->(enterprises) { where('parent_id IN (?) OR child_id IN (?)', enterprises, enterprises) } + scope :permitting, ->(enterprises) { where('child_id IN (?)', enterprises) } + + scope :with_permission, ->(permission) { + joins(:permissions). + where('enterprise_relationship_permissions.name = ?', permission) + } + + scope :by_name, with_enterprises.order('child_enterprises.name, parent_enterprises.name') + def permissions_list=(perms) perms.andand.each { |name| permissions.build name: name } diff --git a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml index 56f0d46c01..6a7dcdc7f4 100644 --- a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml +++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml @@ -1,8 +1,8 @@ -%td {{ enterprise_relationship.parent_name }} +%td {{ enterprise_relationship.child_name }} %td %ul %li{"ng-repeat" => "permission in enterprise_relationship.permissions"} {{ EnterpriseRelationships.permission_presentation(permission.name) }} -%td {{ enterprise_relationship.child_name }} +%td {{ enterprise_relationship.parent_name }} %td.actions %a.delete-enterprise-relationship.icon-trash.no-text{'ng-click' => 'delete(enterprise_relationship)'} diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index b9c2f265cd..86086c8781 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -1,14 +1,13 @@ %tr - %td - %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} - %td - permits - / %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} - / %label - / %input{type: "checkbox", "ng-model" => "permissions[permission]"} - / {{ EnterpriseRelationships.permission_presentation(permission) }} %td %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} + %td + %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} + %label + %input{type: "checkbox", "ng-model" => "permissions[permission]"} + {{ EnterpriseRelationships.permission_presentation(permission) }} + %td + %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} %td.actions %input{type: "button", value: "Create", "ng-click" => "create()"} .errors {{ EnterpriseRelationships.create_errors }} diff --git a/app/views/admin/order_cycles/_exchange_form.html.haml b/app/views/admin/order_cycles/_exchange_form.html.haml index 3539892034..8f81b9f03f 100644 --- a/app/views/admin/order_cycles/_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_form.html.haml @@ -23,4 +23,4 @@ = f.submit 'Add fee', 'ng-click' => 'addExchangeFee($event, exchange)' %td.actions - %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text"} + %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text remove-exchange"} diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index 68dfed9f1a..e859374a3b 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -25,7 +25,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_supplied_products_form' -= select_tag :new_supplier_id, options_from_collection_for_select(Enterprise.is_primary_producer.managed_by(spree_current_user).by_name, :id, :name), {'ng-model' => 'new_supplier_id'} += select_tag :new_supplier_id, options_from_collection_for_select(order_cycle_producer_enterprises, :id, :name), {'ng-model' => 'new_supplier_id'} = f.submit 'Add supplier', 'ng-click' => 'addSupplier($event)' @@ -50,7 +50,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_distributed_products_form' -= select_tag :new_distributor_id, options_from_collection_for_select(Enterprise.is_distributor.managed_by(spree_current_user).by_name, :id, :name), {'ng-model' => 'new_distributor_id'} += select_tag :new_distributor_id, options_from_collection_for_select(order_cycle_hub_enterprises, :id, :name), {'ng-model' => 'new_distributor_id'} = f.submit 'Add distributor', 'ng-click' => 'addDistributor($event)' .actions diff --git a/app/views/admin/order_cycles/show.rep b/app/views/admin/order_cycles/show.rep index 04fc659813..fb71602eb6 100644 --- a/app/views/admin/order_cycles/show.rep +++ b/app/views/admin/order_cycles/show.rep @@ -9,7 +9,7 @@ r.element :order_cycle, @order_cycle do r.element :id end - r.list_of :exchanges, @order_cycle.exchanges.managed_by(spree_current_user).order('id ASC') do |exchange| + r.list_of :exchanges, OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_exchanges(@order_cycle).order('id ASC') do |exchange| r.element :id r.element :sender_id r.element :receiver_id diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb new file mode 100644 index 0000000000..99c0aa0c15 --- /dev/null +++ b/lib/open_food_network/permissions.rb @@ -0,0 +1,37 @@ +module OpenFoodNetwork + class Permissions + def initialize(user) + @user = user + end + + # Find enterprises that an admin is allowed to add to an order cycle + def order_cycle_enterprises + managed_enterprise_ids = managed_enterprises.pluck :id + permitted_enterprise_ids = related_enterprises_with(:add_to_order_cycle).pluck :id + + Enterprise.where('id IN (?)', managed_enterprise_ids + permitted_enterprise_ids) + end + + # Find the exchanges of an order cycle that an admin can manage + def order_cycle_exchanges(order_cycle) + enterprises = managed_enterprises + related_enterprises_with(:add_to_order_cycle) + order_cycle.exchanges.to_enterprises(enterprises).from_enterprises(enterprises) + end + + + private + + def managed_enterprises + Enterprise.managed_by(@user) + end + + def related_enterprises_with(permission) + parent_ids = EnterpriseRelationship. + permitting(managed_enterprises). + with_permission(permission). + pluck(:parent_id) + + Enterprise.where('id IN (?)', parent_ids) + end + end +end diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 757f8bc2e6..7317f44066 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -14,9 +14,9 @@ feature %q{ scenario "listing relationships" do # Given some enterprises with relationships e1, e2, e3, e4 = create(:enterprise), create(:enterprise), create(:enterprise), create(:enterprise) - create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:add_products_to_order_cycle]) + create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:add_to_order_cycle]) create(:enterprise_relationship, parent: e2, child: e3, permissions_list: [:manage_products]) - create(:enterprise_relationship, parent: e3, child: e4, permissions_list: [:add_products_to_order_cycle, :manage_products]) + create(:enterprise_relationship, parent: e3, child: e4, permissions_list: [:add_to_order_cycle, :manage_products]) # When I go to the relationships page click_link 'Enterprises' @@ -24,10 +24,10 @@ feature %q{ # Then I should see the relationships within('table#enterprise-relationships') do - page.should have_relationship e1, e2, ['can add products to order cycle from'] + page.should have_relationship e1, e2, ['can add to order cycle'] page.should have_relationship e2, e3, ['can manage the products of'] page.should have_relationship e3, e4, - ['can add products to order cycle from', 'can manage the products of'] + ['can add to order cycle', 'can manage the products of'] end end @@ -38,16 +38,17 @@ feature %q{ visit admin_enterprise_relationships_path select 'One', from: 'enterprise_relationship_parent_id' - #check 'can add products to order cycle from' - #check 'can manage the products of' - #uncheck 'can manage the products of' + + check 'can add to order cycle' + check 'can manage the products of' + uncheck 'can manage the products of' select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2 #, ['can add products to order cycle from'] + page.should have_relationship e1, e2, ['can add to order cycle'] er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first er.should be_present - #er.permissions.map(&:name).should == ['add_products_to_order_cycle'] + er.permissions.map(&:name).should == ['add_to_order_cycle'] end @@ -119,6 +120,6 @@ feature %q{ def have_relationship(parent, child, perms=[]) perms = perms.join(' ') || 'permits' - have_table_row [parent.name, perms, child.name, ''] + have_table_row [child.name, perms, parent.name, ''] end end diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 2423a6dc05..76ee3bdfc2 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -436,26 +436,37 @@ feature %q{ context "as an enterprise user" do - let(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } - let(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } - let(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } - let(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } - let!(:distributor1_fee) { create(:enterprise_fee, enterprise: distributor1, name: 'First Distributor Fee') } - before(:each) do - product = create(:product, supplier: supplier1) - product.distributors << distributor1 + let!(:supplier_managed) { create(:supplier_enterprise, name: 'Managed supplier') } + let!(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Unmanaged supplier') } + let!(:supplier_permitted) { create(:supplier_enterprise, name: 'Permitted supplier') } + let!(:distributor_managed) { create(:distributor_enterprise, name: 'Managed distributor') } + let!(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Unmanaged Distributor') } + let!(:distributor_permitted) { create(:distributor_enterprise, name: 'Permitted distributor') } + let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor fee') } + let!(:supplier_permitted_relationship) do + create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed, + permissions_list: [:add_to_order_cycle]) + end + let!(:distributor_permitted_relationship) do + create(:enterprise_relationship, parent: distributor_permitted, child: distributor_managed, + permissions_list: [:add_to_order_cycle]) + end + + before do + product = create(:product, supplier: supplier_managed) + product.distributors << distributor_managed product.save! @new_user = create_enterprise_user - @new_user.enterprise_roles.build(enterprise: supplier1).save - @new_user.enterprise_roles.build(enterprise: distributor1).save + @new_user.enterprise_roles.build(enterprise: supplier_managed).save + @new_user.enterprise_roles.build(enterprise: distributor_managed).save login_to_admin_as @new_user end scenario "viewing a list of order cycles I am coordinating" do - oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) - oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier2, name: 'Order Cycle 2' } ) + oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) + oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier_unmanaged, name: 'Order Cycle 2' } ) click_link "Order Cycles" @@ -464,8 +475,8 @@ feature %q{ page.should_not have_content oc_for_other_user.name # The order cycle should not show enterprises that I don't manage - page.should_not have_selector 'td.suppliers', text: supplier2.name - page.should_not have_selector 'td.distributors', text: distributor2.name + page.should_not have_selector 'td.suppliers', text: supplier_unmanaged.name + page.should_not have_selector 'td.distributors', text: distributor_unmanaged.name end scenario "creating a new order cycle" do @@ -476,57 +487,81 @@ feature %q{ fill_in 'order_cycle_orders_open_at', with: '2012-11-06 06:00:00' fill_in 'order_cycle_orders_close_at', with: '2012-11-13 17:00:00' - select 'First Supplier', from: 'new_supplier_id' + select 'Managed supplier', from: 'new_supplier_id' + click_button 'Add supplier' + select 'Permitted supplier', from: 'new_supplier_id' click_button 'Add supplier' - select 'First Distributor', from: 'order_cycle_coordinator_id' + select 'Managed distributor', from: 'order_cycle_coordinator_id' click_button 'Add coordinator fee' - select 'First Distributor Fee', from: 'order_cycle_coordinator_fee_0_id' + select 'Managed distributor fee', from: 'order_cycle_coordinator_fee_0_id' - select 'First Distributor', from: 'new_distributor_id' + select 'Managed distributor', from: 'new_distributor_id' + click_button 'Add distributor' + select 'Permitted distributor', from: 'new_distributor_id' click_button 'Add distributor' - # Should only have suppliers / distributors listed which the user can manage - within "#new_supplier_id" do - page.should_not have_content supplier2.name - end - within "#new_distributor_id" do - page.should_not have_content distributor2.name - end - within "#order_cycle_coordinator_id" do - page.should_not have_content distributor2.name - page.should_not have_content supplier1.name - page.should_not have_content supplier2.name + # Should only have suppliers / distributors listed which the user is managing or + # has E2E permission to add products to order cycles + page.should_not have_select 'new_supplier_id', with_options: [supplier_unmanaged.name] + page.should_not have_select 'new_distributor_id', with_options: [distributor_unmanaged.name] + + [distributor_unmanaged.name, supplier_managed.name, supplier_unmanaged.name].each do |enterprise_name| + page.should_not have_select 'order_cycle_coordinator_id', with_options: [enterprise_name] end click_button 'Create' flash_message.should == "Your order cycle has been created." order_cycle = OrderCycle.find_by_name('My order cycle') - order_cycle.coordinator.should == distributor1 + order_cycle.suppliers.sort.should == [supplier_managed, supplier_permitted].sort + order_cycle.coordinator.should == distributor_managed + order_cycle.distributors.sort.should == [distributor_managed, distributor_permitted].sort end - scenario "editing an order cycle" do - oc = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) + scenario "editing an order cycle does not affect exchanges we don't manage" do + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) visit edit_admin_order_cycle_path(oc) - # I should not see exchanges for supplier2 or distributor2 - page.all('tr.supplier').count.should == 1 - page.all('tr.distributor').count.should == 1 + # I should not see exchanges for supplier_unmanaged or distributor_unmanaged + page.all('tr.supplier').count.should == 2 + page.all('tr.distributor').count.should == 2 # When I save, then those exchanges should remain click_button 'Update' page.should have_content "Your order cycle has been updated." oc.reload - oc.suppliers.sort.should == [supplier1, supplier2] - oc.coordinator.should == supplier1 - oc.distributors.sort.should == [distributor1, distributor2] + oc.suppliers.sort.should == [supplier_managed, supplier_permitted, supplier_unmanaged].sort + oc.coordinator.should == supplier_managed + oc.distributors.sort.should == [distributor_managed, distributor_permitted, distributor_unmanaged].sort end + scenario "editing an order cycle" do + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + + visit edit_admin_order_cycle_path(oc) + + # When I remove all the exchanges and save + page.find("tr.supplier-#{supplier_managed.id} a.remove-exchange").click + page.find("tr.supplier-#{supplier_permitted.id} a.remove-exchange").click + page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click + page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click + click_button 'Update' + + # Then the exchanges should be removed + page.should have_content "Your order cycle has been updated." + + oc.reload + oc.suppliers.should == [supplier_unmanaged] + oc.coordinator.should == supplier_managed + oc.distributors.should == [distributor_unmanaged] + end + + scenario "cloning an order cycle" do - oc = create(:simple_order_cycle) + oc = create(:simple_order_cycle, coordinator: distributor_managed) click_link "Order Cycles" first('a.clone-order-cycle').click diff --git a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee index 4e90f65335..9701377a73 100644 --- a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -12,5 +12,5 @@ describe "enterprise relationships", -> EnterpriseRelationships = _EnterpriseRelationships_ it "presents permission names", -> - expect(EnterpriseRelationships.permission_presentation("add_products_to_order_cycle")).toEqual "can add products to order cycle from" + expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "can add to order cycle" expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "can manage the products of" diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb new file mode 100644 index 0000000000..b0a004cbf1 --- /dev/null +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -0,0 +1,74 @@ +require 'open_food_network/permissions' + +module OpenFoodNetwork + describe Permissions do + let(:user) { double(:user) } + let(:permissions) { Permissions.new(user) } + let(:permission) { 'one' } + let(:e1) { create(:enterprise) } + let(:e2) { create(:enterprise) } + + describe "finding enterprises that can be added to an order cycle" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where('1=0') } + permissions.stub(:related_enterprises_with) { Enterprise.where('1=0') } + end + + it "returns managed enterprises" do + permissions.stub(:managed_enterprises) { Enterprise.where(id: e1) } + permissions.order_cycle_enterprises.should == [e1] + end + + it "returns permitted enterprises" do + permissions.stub(:related_enterprises_with) { Enterprise.where(id: e2) } + permissions.order_cycle_enterprises.should == [e2] + end + end + + describe "finding exchanges of an order cycle that an admin can manage" do + let(:oc) { create(:simple_order_cycle) } + let!(:ex) { create(:exchange, order_cycle: oc, sender: e1, receiver: e2) } + + before do + permissions.stub(:managed_enterprises) { [] } + permissions.stub(:related_enterprises_with) { [] } + end + + it "returns exchanges involving enterprises managed by the user" do + permissions.stub(:managed_enterprises) { [e1, e2] } + permissions.order_cycle_exchanges(oc).should == [ex] + end + + it "returns exchanges involving enterprises with E2E permission" do + permissions.stub(:related_enterprises_with) { [e1, e2] } + permissions.order_cycle_exchanges(oc).should == [ex] + end + + it "does not return exchanges involving only the sender" do + permissions.stub(:managed_enterprises) { [e1] } + permissions.order_cycle_exchanges(oc).should == [] + end + + it "does not return exchanges involving only the receiver" do + permissions.stub(:managed_enterprises) { [e2] } + permissions.order_cycle_exchanges(oc).should == [] + end + end + + ######################################## + + describe "finding related enterprises with a particular permission" do + let!(:er) { create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [permission]) } + + it "returns the enterprises" do + permissions.stub(:managed_enterprises) { e2 } + permissions.send(:related_enterprises_with, permission).should == [e1] + end + + it "returns an empty array when there are none" do + permissions.stub(:managed_enterprises) { e1 } + permissions.send(:related_enterprises_with, permission).should == [] + end + end + end +end diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index 6cdf3db657..3a6c48e891 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -6,10 +6,10 @@ describe EnterpriseRelationship do let(:e2) { create(:enterprise, name: 'B') } let(:e3) { create(:enterprise, name: 'C') } - it "sorts by parent, child enterprise name" do - er1 = create(:enterprise_relationship, parent: e1, child: e3) - er2 = create(:enterprise_relationship, parent: e2, child: e1) - er3 = create(:enterprise_relationship, parent: e1, child: e2) + it "sorts by child, parent enterprise name" do + er1 = create(:enterprise_relationship, parent: e3, child: e1) + er2 = create(:enterprise_relationship, parent: e1, child: e2) + er3 = create(:enterprise_relationship, parent: e2, child: e1) EnterpriseRelationship.by_name.should == [er3, er1, er2] end @@ -43,5 +43,24 @@ describe EnterpriseRelationship do er.permissions.should be_empty end end + + it "finds relationships that grant permissions to some enterprises" do + er1 = create(:enterprise_relationship, parent: e2, child: e1) + er2 = create(:enterprise_relationship, parent: e3, child: e2) + er3 = create(:enterprise_relationship, parent: e1, child: e3) + + EnterpriseRelationship.permitting([e1, e2]).sort.should == [er1, er2] + end + + it "finds relationships that grant a particular permission" do + er1 = create(:enterprise_relationship, parent: e1, child: e2, + permissions_list: ['one', 'two']) + er2 = create(:enterprise_relationship, parent: e2, child: e3, + permissions_list: ['two', 'three']) + er3 = create(:enterprise_relationship, parent: e3, child: e1, + permissions_list: ['three', 'four']) + + EnterpriseRelationship.with_permission('two').sort.should == [er1, er2].sort + end end end From 7f74854a2f72cf668f45c1069ca74d193d8774d3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 28 Aug 2014 15:06:55 +1000 Subject: [PATCH 196/205] For OC, fetch all enterprises we have access to, including those via E2E relationships --- .../javascripts/admin/order_cycle.js.erb.coffee | 2 +- app/controllers/admin/enterprises_controller.rb | 7 ++++++- app/models/spree/ability_decorator.rb | 1 + .../{index.rabl => for_order_cycle.rabl} | 0 config/routes.rb | 7 +++++-- spec/features/admin/order_cycles_spec.rb | 16 ++++++++++++---- spec/javascripts/unit/order_cycle_spec.js.coffee | 2 +- spec/models/spree/ability_spec.rb | 2 +- 8 files changed, 27 insertions(+), 10 deletions(-) rename app/views/admin/enterprises/{index.rabl => for_order_cycle.rabl} (100%) diff --git a/app/assets/javascripts/admin/order_cycle.js.erb.coffee b/app/assets/javascripts/admin/order_cycle.js.erb.coffee index aadf66af60..b8068681ca 100644 --- a/app/assets/javascripts/admin/order_cycle.js.erb.coffee +++ b/app/assets/javascripts/admin/order_cycle.js.erb.coffee @@ -330,7 +330,7 @@ angular.module('order_cycle', ['ngResource']) }]) .factory('Enterprise', ['$resource', ($resource) -> - Enterprise = $resource('/admin/enterprises/:enterprise_id.json', {}, {'index': {method: 'GET', isArray: true}}) + Enterprise = $resource('/admin/enterprises/for_order_cycle/:enterprise_id.json', {}, {'index': {method: 'GET', isArray: true}}) { Enterprise: Enterprise diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 53e58d110b..85fe6831fb 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -6,6 +6,11 @@ module Admin create.after :grant_management helper 'spree/products' + include OrderCyclesHelper + + def for_order_cycle + @collection = order_cycle_permitted_enterprises + end def bulk_update @@ -53,7 +58,7 @@ module Admin end def collection_actions - [:index, :bulk_update] + [:index, :for_order_cycle, :bulk_update] end def load_methods_and_fees diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index b29aa6d5b3..68d98bd568 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -76,6 +76,7 @@ class AbilityDecorator can [:admin, :index, :read, :edit, :update, :bulk_update, :clone], OrderCycle do |order_cycle| user.enterprises.include? order_cycle.coordinator end + can [:for_order_cycle], Enterprise can [:index, :create], EnterpriseFee can [:admin, :read, :edit, :bulk_update, :destroy], EnterpriseFee do |enterprise_fee| diff --git a/app/views/admin/enterprises/index.rabl b/app/views/admin/enterprises/for_order_cycle.rabl similarity index 100% rename from app/views/admin/enterprises/index.rabl rename to app/views/admin/enterprises/for_order_cycle.rabl diff --git a/config/routes.rb b/config/routes.rb index 36213b2622..5230e798cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -33,12 +33,15 @@ Openfoodnetwork::Application.routes.draw do namespace :admin do resources :order_cycles do - post :bulk_update, :on => :collection, :as => :bulk_update + post :bulk_update, on: :collection, as: :bulk_update get :clone, on: :member end resources :enterprises do - post :bulk_update, :on => :collection, :as => :bulk_update + collection do + get :for_order_cycle + post :bulk_update, as: :bulk_update + end resources :producer_properties do post :update_positions, on: :collection diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 76ee3bdfc2..febd966109 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -451,12 +451,10 @@ feature %q{ create(:enterprise_relationship, parent: distributor_permitted, child: distributor_managed, permissions_list: [:add_to_order_cycle]) end + let!(:product_managed) { create(:product, supplier: supplier_managed) } + let!(:product_permitted) { create(:product, supplier: supplier_permitted) } before do - product = create(:product, supplier: supplier_managed) - product.distributors << distributor_managed - product.save! - @new_user = create_enterprise_user @new_user.enterprise_roles.build(enterprise: supplier_managed).save @new_user.enterprise_roles.build(enterprise: distributor_managed).save @@ -492,6 +490,9 @@ feature %q{ select 'Permitted supplier', from: 'new_supplier_id' click_button 'Add supplier' + select_incoming_variant supplier_managed, 0, product_managed.master + select_incoming_variant supplier_permitted, 1, product_permitted.master + select 'Managed distributor', from: 'order_cycle_coordinator_id' click_button 'Add coordinator fee' select 'Managed distributor fee', from: 'order_cycle_coordinator_fee_0_id' @@ -574,4 +575,11 @@ feature %q{ end + + private + + def select_incoming_variant(supplier, exchange_no, variant) + page.find("table.exchanges tr.supplier-#{supplier.id} td.products input").click + check "order_cycle_incoming_exchange_#{exchange_no}_variants_#{variant.id}" + end end diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 172b2cdb1a..0d5127314a 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -327,7 +327,7 @@ describe 'OrderCycle services', -> inject ($injector, _$httpBackend_)-> Enterprise = $injector.get('Enterprise') $httpBackend = _$httpBackend_ - $httpBackend.whenGET('/admin/enterprises.json').respond [ + $httpBackend.whenGET('/admin/enterprises/for_order_cycle.json').respond [ {id: 1, name: 'One', supplied_products: [1, 2]} {id: 2, name: 'Two', supplied_products: [3, 4]} {id: 3, name: 'Three', supplied_products: [5, 6]} diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index a2d1fb5bdf..9e5ae49780 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -264,7 +264,7 @@ module Spree end it 'should have the ability administrate and create enterpises' do - should have_ability([:admin, :index, :create], for: Enterprise) + should have_ability([:admin, :index, :for_order_cycle, :create], for: Enterprise) end end end From 62e6cacfd0d7db7e043c9b6aa77cf834d37e2c85 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 29 Aug 2014 17:37:56 +1000 Subject: [PATCH 197/205] Rename spec to match view name change --- .../{index.rabl_spec.rb => for_order_cycle.rabl_spec.rb} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename spec/views/admin/enterprises/{index.rabl_spec.rb => for_order_cycle.rabl_spec.rb} (79%) diff --git a/spec/views/admin/enterprises/index.rabl_spec.rb b/spec/views/admin/enterprises/for_order_cycle.rabl_spec.rb similarity index 79% rename from spec/views/admin/enterprises/index.rabl_spec.rb rename to spec/views/admin/enterprises/for_order_cycle.rabl_spec.rb index 91463c2212..1585d08e43 100644 --- a/spec/views/admin/enterprises/index.rabl_spec.rb +++ b/spec/views/admin/enterprises/for_order_cycle.rabl_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe "admin/enterprises/index.rabl" do +describe "admin/enterprises/for_order_cycle.rabl" do let(:enterprise) { create(:distributor_enterprise) } let!(:product) { create(:simple_product, supplier: enterprise) } let!(:deleted_product) { create(:simple_product, supplier: enterprise, deleted_at: 1.day.ago) } - let(:render) { Rabl.render([enterprise], 'admin/enterprises/index', view_path: 'app/views', scope: RablHelper::FakeContext.instance) } + let(:render) { Rabl.render([enterprise], 'admin/enterprises/for_order_cycle', view_path: 'app/views', scope: RablHelper::FakeContext.instance) } describe "supplied products" do it "does not render deleted products" do From 66f20a6b8a8acf3d6e4c52ad1f205f478b2d11d5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 1 Sep 2014 09:17:38 +1000 Subject: [PATCH 198/205] Name test enterprises semantically --- .../admin/bulk_product_update_spec.rb | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index ccddba5508..d1dfc2b129 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -729,20 +729,20 @@ feature %q{ end context "as an enterprise manager" do - let(:s1) { create(:supplier_enterprise, name: 'First Supplier') } - let(:s2) { create(:supplier_enterprise, name: 'Another Supplier') } - let(:s3) { create(:supplier_enterprise, name: 'Yet Another Supplier') } - let(:d1) { create(:distributor_enterprise, name: 'First Distributor') } - let(:d2) { create(:distributor_enterprise, name: 'Another Distributor') } - let!(:product_supplied) { create(:product, supplier: s1, price: 10.0, on_hand: 6) } - let!(:product_not_supplied) { create(:product, supplier: s3) } - let(:product_supplied_inactive) { create(:product, supplier: s1, price: 10.0, on_hand: 6, available_on: 1.week.from_now) } + let(:supplier_managed1) { create(:supplier_enterprise, name: 'Supplier Managed 1') } + let(:supplier_managed2) { create(:supplier_enterprise, name: 'Supplier Managed 2') } + let(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Supplier Unmanaged') } + let(:distributor_managed) { create(:distributor_enterprise, name: 'Distributor Managed') } + let(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Distributor Unmanaged') } + let!(:product_supplied) { create(:product, supplier: supplier_managed1, price: 10.0, on_hand: 6) } + let!(:product_not_supplied) { create(:product, supplier: supplier_unmanaged) } + let(:product_supplied_inactive) { create(:product, supplier: supplier_managed1, price: 10.0, on_hand: 6, available_on: 1.week.from_now) } - before(:each) do + before do @enterprise_user = create_enterprise_user - @enterprise_user.enterprise_roles.build(enterprise: s1).save - @enterprise_user.enterprise_roles.build(enterprise: s2).save - @enterprise_user.enterprise_roles.build(enterprise: d1).save + @enterprise_user.enterprise_roles.build(enterprise: supplier_managed1).save + @enterprise_user.enterprise_roles.build(enterprise: supplier_managed2).save + @enterprise_user.enterprise_roles.build(enterprise: distributor_managed).save login_to_admin_as @enterprise_user end @@ -757,8 +757,8 @@ feature %q{ it "shows only suppliers that I manage" do visit '/admin/products/bulk_edit' - expect(page).to have_select 'producer', with_options: [s1.name, s2.name], selected: s1.name - expect(page).to have_no_select 'producer', with_options: [s3.name] + expect(page).to have_select 'producer', with_options: [supplier_managed1.name, supplier_managed2.name], selected: supplier_managed1.name + expect(page).to have_no_select 'producer', with_options: [supplier_unmanaged.name] end it "shows inactive products that I supply" do @@ -777,13 +777,13 @@ feature %q{ first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click expect(page).to have_field "product_name", with: p.name - expect(page).to have_select "producer", selected: s1.name + expect(page).to have_select "producer", selected: supplier_managed1.name expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") expect(page).to have_field "price", with: "10.0" expect(page).to have_field "on_hand", with: "6" fill_in "product_name", with: "Big Bag Of Potatoes" - select(s2.name, :from => 'producer') + select(supplier_managed2.name, :from => 'producer') fill_in "available_on", with: (Date.today-3).strftime("%F %T") fill_in "price", with: "20" select "Weight (kg)", from: "variant_unit_with_scale" @@ -795,7 +795,7 @@ feature %q{ p.reload expect(p.name).to eq "Big Bag Of Potatoes" - expect(p.supplier).to eq s2 + expect(p.supplier).to eq supplier_managed2 expect(p.variant_unit).to eq "weight" expect(p.variant_unit_scale).to eq 1000 # Kg expect(p.available_on).to eq 3.days.ago.beginning_of_day From e0645dfbd91e12d16e2833df985332bcf9b11245 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 1 Sep 2014 09:42:50 +1000 Subject: [PATCH 199/205] Fetch managed products via OpenFoodNetwork::Permissions --- .../spree/api/products_controller_decorator.rb | 8 +++++++- lib/open_food_network/permissions.rb | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/controllers/spree/api/products_controller_decorator.rb b/app/controllers/spree/api/products_controller_decorator.rb index 7e81e1f789..65765e03ca 100644 --- a/app/controllers/spree/api/products_controller_decorator.rb +++ b/app/controllers/spree/api/products_controller_decorator.rb @@ -1,3 +1,5 @@ +require 'open_food_network/permissions' + Spree::Api::ProductsController.class_eval do def managed authorize! :admin, Spree::Product @@ -8,7 +10,11 @@ Spree::Api::ProductsController.class_eval do end def bulk_products - @products = product_scope.ransack(params[:q]).result.managed_by(current_api_user).page(params[:page]).per(params[:per_page]) + @products = OpenFoodNetwork::Permissions.new(current_api_user).managed_products. + merge(product_scope). + ransack(params[:q]).result. + page(params[:page]).per(params[:per_page]) + render text: { products: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer), pages: @products.num_pages }.to_json end diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 99c0aa0c15..fbd85d933b 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -18,6 +18,10 @@ module OpenFoodNetwork order_cycle.exchanges.to_enterprises(enterprises).from_enterprises(enterprises) end + def managed_products + Spree::Product.managed_by(@user) + end + private From cfb31b46e48fb765976b27c04b99ffcd8ceba997 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 1 Sep 2014 10:13:59 +1000 Subject: [PATCH 200/205] Bulk product edit lists managed products --- lib/open_food_network/permissions.rb | 12 ++++++- .../admin/bulk_product_update_spec.rb | 26 +++++++++++----- .../lib/open_food_network/permissions_spec.rb | 31 +++++++++++++++++++ 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index fbd85d933b..f35a7db602 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -19,7 +19,9 @@ module OpenFoodNetwork end def managed_products - Spree::Product.managed_by(@user) + managed_enterprise_products_ids = managed_enterprise_products.pluck :id + permitted_enterprise_products_ids = related_enterprise_products.pluck :id + Spree::Product.where('id IN (?)', managed_enterprise_products_ids + permitted_enterprise_products_ids) end @@ -37,5 +39,13 @@ module OpenFoodNetwork Enterprise.where('id IN (?)', parent_ids) end + + def managed_enterprise_products + Spree::Product.managed_by(@user) + end + + def related_enterprise_products + Spree::Product.where('supplier_id IN (?)', related_enterprises_with(:manage_products)) + end end end diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index d1dfc2b129..90f83bb26d 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -732,12 +732,21 @@ feature %q{ let(:supplier_managed1) { create(:supplier_enterprise, name: 'Supplier Managed 1') } let(:supplier_managed2) { create(:supplier_enterprise, name: 'Supplier Managed 2') } let(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Supplier Unmanaged') } + let(:supplier_permitted) { create(:supplier_enterprise, name: 'Supplier Permitted') } let(:distributor_managed) { create(:distributor_enterprise, name: 'Distributor Managed') } let(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Distributor Unmanaged') } let!(:product_supplied) { create(:product, supplier: supplier_managed1, price: 10.0, on_hand: 6) } let!(:product_not_supplied) { create(:product, supplier: supplier_unmanaged) } + let!(:product_supplied_permitted) { create(:product, name: 'Product Permitted', supplier: supplier_permitted, price: 10.0, on_hand: 6) } let(:product_supplied_inactive) { create(:product, supplier: supplier_managed1, price: 10.0, on_hand: 6, available_on: 1.week.from_now) } + let!(:supplier_permitted_relationship) do + create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed1, + permissions_list: [:manage_products]) + end + + use_short_wait + before do @enterprise_user = create_enterprise_user @enterprise_user.enterprise_roles.build(enterprise: supplier_managed1).save @@ -751,6 +760,7 @@ feature %q{ visit '/admin/products/bulk_edit' expect(page).to have_field 'product_name', with: product_supplied.name + expect(page).to have_field 'product_name', with: product_supplied_permitted.name expect(page).to have_no_field 'product_name', with: product_not_supplied.name end @@ -782,13 +792,15 @@ feature %q{ expect(page).to have_field "price", with: "10.0" expect(page).to have_field "on_hand", with: "6" - fill_in "product_name", with: "Big Bag Of Potatoes" - select(supplier_managed2.name, :from => 'producer') - fill_in "available_on", with: (Date.today-3).strftime("%F %T") - fill_in "price", with: "20" - select "Weight (kg)", from: "variant_unit_with_scale" - fill_in "on_hand", with: "18" - fill_in "display_as", with: "Big Bag" + within("tr#p_#{product_supplied.id}") do + fill_in "product_name", with: "Big Bag Of Potatoes" + select(supplier_managed2.name, :from => 'producer') + fill_in "available_on", with: (Date.today-3).strftime("%F %T") + fill_in "price", with: "20" + select "Weight (kg)", from: "variant_unit_with_scale" + fill_in "on_hand", with: "18" + fill_in "display_as", with: "Big Bag" + end click_button 'Save Changes' expect(page.find("#update-status-message")).to have_content "Changes saved." diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index b0a004cbf1..bbab0d7175 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -55,6 +55,26 @@ module OpenFoodNetwork end end + describe "finding managed products" do + let!(:p1) { create(:simple_product) } + let!(:p2) { create(:simple_product) } + + before do + permissions.stub(:managed_enterprise_products) { Spree::Product.where('1=0') } + permissions.stub(:related_enterprise_products) { Spree::Product.where('1=0') } + end + + it "returns products produced by managed enterprises" do + permissions.stub(:managed_enterprise_products) { Spree::Product.where(id: p1) } + permissions.managed_products.should == [p1] + end + + it "returns products produced by permitted enterprises" do + permissions.stub(:related_enterprise_products) { Spree::Product.where(id: p2) } + permissions.managed_products.should == [p2] + end + end + ######################################## describe "finding related enterprises with a particular permission" do @@ -70,5 +90,16 @@ module OpenFoodNetwork permissions.send(:related_enterprises_with, permission).should == [] end end + + describe "finding the supplied products of related enterprises" do + let!(:e) { create(:enterprise) } + let!(:p) { create(:simple_product, supplier: e) } + + it "returns supplied products" do + permissions.should_receive(:related_enterprises_with).with(:manage_products) { [e] } + + permissions.send(:related_enterprise_products).should == [p] + end + end end end From 4d8d74dec7d230660ef9bce6e94da56272de1f4b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 1 Sep 2014 10:36:59 +1000 Subject: [PATCH 201/205] Find enterprises that we manage products for --- lib/open_food_network/permissions.rb | 17 +++++-- .../lib/open_food_network/permissions_spec.rb | 49 ++++++++++++++----- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index f35a7db602..3d5df1e804 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -6,10 +6,7 @@ module OpenFoodNetwork # Find enterprises that an admin is allowed to add to an order cycle def order_cycle_enterprises - managed_enterprise_ids = managed_enterprises.pluck :id - permitted_enterprise_ids = related_enterprises_with(:add_to_order_cycle).pluck :id - - Enterprise.where('id IN (?)', managed_enterprise_ids + permitted_enterprise_ids) + managed_and_related_enterprises_with :add_to_order_cycle end # Find the exchanges of an order cycle that an admin can manage @@ -24,6 +21,10 @@ module OpenFoodNetwork Spree::Product.where('id IN (?)', managed_enterprise_products_ids + permitted_enterprise_products_ids) end + def managed_product_enterprises + managed_and_related_enterprises_with :manage_products + end + private @@ -40,6 +41,14 @@ module OpenFoodNetwork Enterprise.where('id IN (?)', parent_ids) end + def managed_and_related_enterprises_with(permission) + managed_enterprise_ids = managed_enterprises.pluck :id + permitted_enterprise_ids = related_enterprises_with(permission).pluck :id + + Enterprise.where('id IN (?)', managed_enterprise_ids + permitted_enterprise_ids) + end + + def managed_enterprise_products Spree::Product.managed_by(@user) end diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index bbab0d7175..bcd1f8b742 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -9,19 +9,15 @@ module OpenFoodNetwork let(:e2) { create(:enterprise) } describe "finding enterprises that can be added to an order cycle" do - before do - permissions.stub(:managed_enterprises) { Enterprise.where('1=0') } - permissions.stub(:related_enterprises_with) { Enterprise.where('1=0') } - end + let(:e) { double(:enterprise) } - it "returns managed enterprises" do - permissions.stub(:managed_enterprises) { Enterprise.where(id: e1) } - permissions.order_cycle_enterprises.should == [e1] - end + it "returns managed and related enterprises with add_to_order_cycle permission" do + permissions. + should_receive(:managed_and_related_enterprises_with). + with(:add_to_order_cycle). + and_return([e]) - it "returns permitted enterprises" do - permissions.stub(:related_enterprises_with) { Enterprise.where(id: e2) } - permissions.order_cycle_enterprises.should == [e2] + permissions.order_cycle_enterprises.should == [e] end end @@ -75,6 +71,19 @@ module OpenFoodNetwork end end + describe "finding enterprises that we manage products for" do + let(:e) { double(:enterprise) } + + it "returns managed and related enterprises with manage_products permission" do + permissions. + should_receive(:managed_and_related_enterprises_with). + with(:manage_products). + and_return([e]) + + permissions.managed_product_enterprises.should == [e] + end + end + ######################################## describe "finding related enterprises with a particular permission" do @@ -91,6 +100,24 @@ module OpenFoodNetwork end end + describe "finding enterprises that are managed or with a particular permission" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where('1=0') } + permissions.stub(:related_enterprises_with) { Enterprise.where('1=0') } + end + + it "returns managed enterprises" do + permissions.should_receive(:managed_enterprises) { Enterprise.where(id: e1) } + permissions.send(:managed_and_related_enterprises_with, permission).should == [e1] + end + + it "returns permitted enterprises" do + permissions.should_receive(:related_enterprises_with).with(permission). + and_return(Enterprise.where(id: e2)) + permissions.send(:managed_and_related_enterprises_with, permission).should == [e2] + end + end + describe "finding the supplied products of related enterprises" do let!(:e) { create(:enterprise) } let!(:p) { create(:simple_product, supplier: e) } From c81503d95f3939e20fc335b6458e73cb5e6b98cb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 1 Sep 2014 10:44:09 +1000 Subject: [PATCH 202/205] Include producers I have permission to in BPE producers choice --- app/controllers/spree/admin/products_controller_decorator.rb | 2 +- spec/features/admin/bulk_product_update_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 9f4fdbe46b..7fc9984241 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -85,7 +85,7 @@ Spree::Admin::ProductsController.class_eval do def load_bpe_data current_user.generate_spree_api_key! unless spree_current_user.spree_api_key @spree_api_key = spree_current_user.spree_api_key - @producers = Enterprise.managed_by(spree_current_user).is_primary_producer.order(:name) + @producers = OpenFoodNetwork::Permissions.new(spree_current_user).managed_product_enterprises.is_primary_producer.by_name @taxons = Spree::Taxon.order(:name) end end diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 90f83bb26d..c4dbba32fd 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -764,10 +764,10 @@ feature %q{ expect(page).to have_no_field 'product_name', with: product_not_supplied.name end - it "shows only suppliers that I manage" do + it "shows only suppliers that I manage or have permission to" do visit '/admin/products/bulk_edit' - expect(page).to have_select 'producer', with_options: [supplier_managed1.name, supplier_managed2.name], selected: supplier_managed1.name + expect(page).to have_select 'producer', with_options: [supplier_managed1.name, supplier_managed2.name, supplier_permitted.name], selected: supplier_managed1.name expect(page).to have_no_select 'producer', with_options: [supplier_unmanaged.name] end From 94683f1eaa153e1287665902f95ee92fbc5aca3d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 1 Sep 2014 11:15:00 +1000 Subject: [PATCH 203/205] Check authorisation for bulk update products --- .../admin/products_controller_decorator.rb | 3 +++ .../spree/admin/products_controller_spec.rb | 21 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 7fc9984241..a5b38126e5 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -30,6 +30,9 @@ Spree::Admin::ProductsController.class_eval do "#{string}q[#{filter[:property][:db_column]}_#{filter[:predicate][:predicate]}]=#{filter[:value]};" end + # Ensure we're authorised to update all products + product_set.collection.each { |p| authorize! :update, p } + if product_set.save redirect_to "/api/products/bulk_products?page=1;per_page=500;#{bulk_index_query}" else diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb index f05be2bb69..9746ffa0e2 100644 --- a/spec/controllers/spree/admin/products_controller_spec.rb +++ b/spec/controllers/spree/admin/products_controller_spec.rb @@ -1,11 +1,28 @@ require 'spec_helper' describe Spree::Admin::ProductsController do - context "Creating a new product" do + describe "updating a product we do not have access to" do + let(:s_managed) { create(:enterprise) } + let(:s_unmanaged) { create(:enterprise) } + let(:p) { create(:simple_product, supplier: s_unmanaged, name: 'Peas') } + before do - login_as_admin + login_as_enterprise_user [s_managed] + spree_post :bulk_update, {"products" => [{"id" => p.id, "name" => "Pine nuts"}]} end + it "denies access" do + response.should redirect_to "http://test.host/unauthorized" + end + + it "does not update any product" do + p.reload.name.should_not == "Pine nuts" + end + end + + context "creating a new product" do + before { login_as_admin } + it "redirects to bulk_edit when the user hits 'create'" do s = create(:supplier_enterprise) t = create(:taxon) From e72c3d861b5b8a579163282bd151a88e8b91238c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 1 Sep 2014 11:49:09 +1000 Subject: [PATCH 204/205] Enterprise manager can edit products from enterprises it has manage_products permission on --- app/models/spree/ability_decorator.rb | 4 ++-- spec/features/admin/bulk_product_update_spec.rb | 14 +++++++------- spec/models/spree/ability_spec.rb | 17 ++++++++++++++--- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 68d98bd568..9f9c96cb15 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -43,12 +43,12 @@ class AbilityDecorator # Enterprise User can only access products that they are a supplier for can [:create], Spree::Product can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], Spree::Product do |product| - user.enterprises.include? product.supplier + OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier end can [:create], Spree::Variant can [:admin, :index, :read, :edit, :update, :search, :destroy], Spree::Variant do |variant| - user.enterprises.include? variant.product.supplier + OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? variant.product.supplier end can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index c4dbba32fd..9f530f860c 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -780,19 +780,19 @@ feature %q{ end it "allows me to update a product" do - p = product_supplied + p = product_supplied_permitted visit '/admin/products/bulk_edit' first("div#columns_dropdown", :text => "COLUMNS").click first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click - expect(page).to have_field "product_name", with: p.name - expect(page).to have_select "producer", selected: supplier_managed1.name - expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") - expect(page).to have_field "price", with: "10.0" - expect(page).to have_field "on_hand", with: "6" + within "tr#p_#{p.id}" do + expect(page).to have_field "product_name", with: p.name + expect(page).to have_select "producer", selected: supplier_permitted.name + expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") + expect(page).to have_field "price", with: "10.0" + expect(page).to have_field "on_hand", with: "6" - within("tr#p_#{product_supplied.id}") do fill_in "product_name", with: "Big Bag Of Potatoes" select(supplier_managed2.name, :from => 'producer') fill_in "available_on", with: (Date.today-3).strftime("%F %T") diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 9e5ae49780..cf17d7fe3a 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -51,14 +51,17 @@ module Spree # create enterprises let(:s1) { create(:supplier_enterprise) } let(:s2) { create(:supplier_enterprise) } + let(:s_related) { create(:supplier_enterprise) } let(:d1) { create(:distributor_enterprise) } let(:d2) { create(:distributor_enterprise) } let(:p1) { create(:product, supplier: s1, distributors:[d1, d2]) } let(:p2) { create(:product, supplier: s2, distributors:[d1, d2]) } + let(:p_related) { create(:product, supplier: s_related) } let(:er1) { create(:enterprise_relationship, parent: s1, child: d1) } let(:er2) { create(:enterprise_relationship, parent: d1, child: s1) } + let(:er_p) { create(:enterprise_relationship, parent: s_related, child: s1, permissions_list: [:manage_products]) } subject { user } let(:user) { nil } @@ -74,12 +77,20 @@ module Spree let(:order) {create(:order)} - it "should be able to read/write their enterprises' products" do + it "should be able to read/write their enterprises' products and variants" do should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p1) + should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p1.master) end - it "should not be able to read/write other enterprises' products" do + it "should be able to read/write related enterprises' products and variants with manage_products permission" do + er_p + should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p_related) + should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p_related.master) + end + + it "should not be able to read/write other enterprises' products and variants" do should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p2) + should_not have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p2.master) end it "should not be able to access admin actions on orders" do @@ -247,7 +258,7 @@ module Spree end end - context 'Enterprise manager' do + context 'enterprise manager' do let (:user) do user = create(:user) user.spree_roles = [] From 2cd5afbf9ccbc68fa86f6635110d197c4e242e23 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 1 Sep 2014 16:21:23 +1000 Subject: [PATCH 205/205] Set product.largeImage in JS, use for product modal --- .../darkswarm/services/products.js.coffee | 1 + .../javascripts/templates/product_modal.html.haml | 2 +- .../unit/darkswarm/services/product_spec.js.coffee | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/services/products.js.coffee b/app/assets/javascripts/darkswarm/services/products.js.coffee index 5819c7b661..9da3187f82 100644 --- a/app/assets/javascripts/darkswarm/services/products.js.coffee +++ b/app/assets/javascripts/darkswarm/services/products.js.coffee @@ -48,3 +48,4 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Car product.primaryImage = product.images[0]?.small_url if product.images product.primaryImageOrMissing = product.primaryImage || "/assets/noimage/small.png" + product.largeImage = product.images[0]?.large_url if product.images diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index 31f51c6655..82cda0a371 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -1,6 +1,6 @@ .row .columns.small-12.large-6 - %img.product-img{"ng-src" => "{{product.primaryImage}}", "ng-if" => "product.primaryImage"} + %img.product-img{"ng-src" => "{{product.largeImage}}", "ng-if" => "product.largeImage"} .columns.small-12.large-6.product-header %h2 / %render-svg{path: "{{product.primary_taxon.icon}}"} diff --git a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee index d7c5a20b91..27e3f5aff6 100644 --- a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee @@ -7,6 +7,7 @@ describe 'Products service', -> CurrentHubMock = {} currentOrder = null product = null + productWithImage = null beforeEach -> product = @@ -16,6 +17,14 @@ describe 'Products service', -> price: 11 master: {} variants: [] + productWithImage = + supplier: + id: 9 + master: {} + variants: [] + images: [ + large_url: 'foo.png' + ] currentOrder = line_items: [] @@ -62,6 +71,11 @@ describe 'Products service', -> expect(Products.products[0].primaryImage).toBeUndefined() expect(Products.products[0].primaryImageOrMissing).toEqual "/assets/noimage/small.png" + it "sets largeImage", -> + $httpBackend.expectGET("/shop/products").respond([productWithImage]) + $httpBackend.flush() + expect(Products.products[0].largeImage).toEqual("foo.png") + describe "determining the price to display for a product", -> it "displays the product price when the product does not have variants", -> $httpBackend.expectGET("/shop/products").respond([product])