From e9fca885db0809751013a120374f67bf405d4372 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 25 Feb 2015 10:46:43 +1100 Subject: [PATCH 01/81] WIP layout changes for shopfront top --- app/views/shop/products/_form.html.haml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index e247d7e3f2..48436881a7 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -2,19 +2,15 @@ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} .row - .small-12.medium-8.large-9.columns + .small-12.medium-6.large-5.columns %input#search.text{"ng-model" => "query", placeholder: "Search by product or producer", "ng-debounce" => "100", "ofn-disable-enter" => true} + .small-12.medium-6.large-6.large-offset-1.columns = render partial: "shop/products/filters" - %form{action: cart_path} - .small-12.medium-4.large-3.columns - %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} - %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } - %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons) track by product.id ", "id" => "product-{{ product.id }}"} From 304bde3b9e34d375cb2d04d9f7b5d5b77d064057 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 25 Feb 2015 10:46:51 +1100 Subject: [PATCH 02/81] Working on the filters for shopfront properties and updates. --- app/views/shop/products/_filters.html.haml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 395e6f123d..0ed6f0c7d1 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -4,14 +4,14 @@ .row.filter-box.filter-box-shopfront.animate-hide{"ng-show" => "filtersActive"} .small-12.columns - .row.tdhead - .small-12.medium-6.columns - %h5 - .light Filter by - Category - .small-12.medium-6.columns.text-right - = render partial: 'shared/components/filter_box_shopfront' - = render partial: 'shared/components/filter_controls_shopfront' + / .row.tdhead + / .small-12.medium-6.columns + / %h5 + / .light Filter by + / Category + / .small-12.medium-6.columns.text-right + / = render partial: 'shared/components/filter_box_shopfront' + / = render partial: 'shared/components/filter_controls_shopfront' .row .small-12.columns From d43f367f3089f05ff3b553a46641f3399dc49f9c Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 25 Feb 2015 11:11:09 +1100 Subject: [PATCH 03/81] Shopfront Properties - WIP layout work --- app/assets/stylesheets/darkswarm/active_table_search.css.sass | 2 ++ app/assets/stylesheets/darkswarm/shop.css.sass | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/active_table_search.css.sass b/app/assets/stylesheets/darkswarm/active_table_search.css.sass index 635d2e2e4b..7b76d2643e 100644 --- a/app/assets/stylesheets/darkswarm/active_table_search.css.sass +++ b/app/assets/stylesheets/darkswarm/active_table_search.css.sass @@ -21,6 +21,8 @@ .row.filter-box.filter-box-shopfront margin-top: 0 + background: transparent + border: 0 none products .filter-box background: #f7f7f7 diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 89557fbbec..9d38398f36 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -15,10 +15,8 @@ products display: block - padding-top: 2.3em + padding-top: 20px @media all and (max-width: 768px) - padding-top: 1em - input.button.right float: left From 15144bddddd1227f06b57da2baee22b54d6880d4 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 25 Feb 2015 13:18:45 +1100 Subject: [PATCH 04/81] WIP search input on shopfront page. --- .../darkswarm/_shop-inputs.css.sass | 3 +- .../stylesheets/darkswarm/big-input.sass | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-inputs.css.sass b/app/assets/stylesheets/darkswarm/_shop-inputs.css.sass index f11c31a153..b191913020 100644 --- a/app/assets/stylesheets/darkswarm/_shop-inputs.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-inputs.css.sass @@ -9,8 +9,7 @@ @include placeholder(rgba(0,0,0,0.4), #777) input#search - @include big-input(rgba(0,0,0,0.3), #777, $clr-brick) - @include big-input-static + @include medium-input(rgba(0,0,0,0.3), #777, $clr-brick) // ordering product diff --git a/app/assets/stylesheets/darkswarm/big-input.sass b/app/assets/stylesheets/darkswarm/big-input.sass index 15bf937f39..90bba8cec2 100644 --- a/app/assets/stylesheets/darkswarm/big-input.sass +++ b/app/assets/stylesheets/darkswarm/big-input.sass @@ -13,7 +13,7 @@ border: 2px solid $input font-size: 2rem box-shadow: 0 - padding: 0.75rem 1rem 0.35rem 1rem + padding: 0.75rem 1rem 0.35rem height: auto width: 100% margin-bottom: 0.5rem @@ -33,8 +33,9 @@ background: white background: rgba(255,255,255,0.5) text-shadow: 0 0 10px #ffffff - padding: 1.5rem 1rem 1rem 1rem + padding: 1.5rem 1rem 1rem letter-spacing: 0.02rem + outline: none @mixin big-input-static outline: 0 @@ -42,6 +43,36 @@ padding: 0.75rem 1rem 0.35rem 1rem letter-spacing: 0 +@mixin medium-input($input, $inputhvr, $inputactv) + @include avenir + @include csstrans + @include border-radius(0.5rem) + background: rgba(255,255,255,0.1) + border: 2px solid $input + font-size: 1rem + box-shadow: 0 + padding: 0.75rem 1rem 0.35rem + height: auto + width: 100% + margin-bottom: 0.5rem + box-shadow: none + color: $inputactv + + &:hover + @include box-shadow(0 1px 1px 0 rgba(255,255,255,0.25)) + border: 2px solid $inputhvr + color: $inputactv + + &:active, &:focus, &.active + border: 2px solid $inputactv + color: $inputactv + background: white + background: rgba(255,255,255,0.5) + text-shadow: 0 0 10px #ffffff + padding: 1em 1rem 0.75rem + letter-spacing: 0.02rem + outline: none + @mixin placeholder($placeholder, $placeholderhvr) ::-webkit-input-placeholder color: $placeholder From 469b383781e1cfedb364a6785d61cde1a0917498 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 25 Feb 2015 16:04:32 +1100 Subject: [PATCH 05/81] Properties and shopfront filters with taxons WIP. --- .../darkswarm/_shop-filters.css.sass | 92 +++++++++++++++++++ .../darkswarm/active_table_search.css.sass | 12 --- app/views/shop/products/_filters.html.haml | 62 +++++++++---- app/views/shop/products/_form.html.haml | 16 ++++ 4 files changed, 151 insertions(+), 31 deletions(-) create mode 100644 app/assets/stylesheets/darkswarm/_shop-filters.css.sass diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass new file mode 100644 index 0000000000..fbebe3a431 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -0,0 +1,92 @@ +@import mixins +@import branding +@import big-input +@import animations + +// Alert when search, taxon, filter is triggered + +.alert-box.search-alert + background-color: #faf6c7 + border-color: #faf6c7 + color: #888 + font-size: 0.75rem + + span.applied-properties + color: #333 + + span.applied-taxons + color: $clr-blue + + span.applied-search + color: $clr-brick + + span.filter-label + opacity: 0.75 + +// Shopfront taxons +.filter-box.filter-box-shopfront, .filter-box.property-box-shopfront + background: transparent + + ul + margin: 0 + ul, ul li + list-style: none + li + display: inline-block + @include border-radius(0) + padding: 0 + margin: 0 0 0.25rem 0.25rem + &:hover, &:focus + background: transparent + + li.active + @include box-shadow(none) + a, a:hover, a:focus, a:active, a.active + border: 1px solid $clr-blue + background: $clr-blue + color: white + render-svg + svg + path + fill: white + li a + @include border-radius(0.5em) + border: 1px solid $clr-blue-light + padding: 0.625em + font-size: 0.875em + color: $clr-blue + font-size: 0.75em + i + font-size: 1.25rem + margin-left: 0.25rem + render-svg + width: 1.25rem + height: 1.25rem + svg + width: 1.25rem + height: 1.25rem + path + @include csstrans + fill: $clr-blue + &:hover, &:focus + color: $clr-blue-bright + render-svg + svg + path + fill: $clr-blue-bright + +// Shopfront properties +.filter-box.property-box-shopfront + li.active + @include box-shadow(none) + a, a:hover, a:focus, a:active, a.active + border: 1px solid #333 + background: #333 + color: white + li a + border: 1px solid #ccc + color: #888 + &:hover, &:focus + border: 1px solid #999 + color: #666 + diff --git a/app/assets/stylesheets/darkswarm/active_table_search.css.sass b/app/assets/stylesheets/darkswarm/active_table_search.css.sass index 7b76d2643e..d19f116139 100644 --- a/app/assets/stylesheets/darkswarm/active_table_search.css.sass +++ b/app/assets/stylesheets/darkswarm/active_table_search.css.sass @@ -27,7 +27,6 @@ products .filter-box background: #f7f7f7 - .filter-box background: rgba(245,245,245,0.6) .tdhead @@ -42,7 +41,6 @@ products .filter-box [class*="block-grid-"] > li padding-bottom: 0.5rem !important - li @include border-radius(12px) padding-top: 0.5rem @@ -107,16 +105,6 @@ products .filter-box path fill: #666 -.filter-box.filter-box-shopfront - .tdhead - margin-top: 0rem - margin-bottom: 0.75rem - padding: 0.5rem 0 - h5 - color: $clr-blue - .button.tiny - margin-bottom: 0rem - .button.filterbtn margin-bottom: 0 !important min-width: 160px diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 0ed6f0c7d1..61f807c684 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,21 +1,45 @@ -.row.animate-show{"ng-hide" => "filtersActive"} - .small-12.columns - = render partial: 'shared/components/filter_controls_shopfront' +.filter-box.filter-box-shopfront.animate-hide.text-right + %ul + %taxon-selector{objects: "Products.products | products:query | products:showProfiles", + results: "activeTaxons"} -.row.filter-box.filter-box-shopfront.animate-hide{"ng-show" => "filtersActive"} - .small-12.columns - / .row.tdhead - / .small-12.medium-6.columns - / %h5 - / .light Filter by - / Category - / .small-12.medium-6.columns.text-right - / = render partial: 'shared/components/filter_box_shopfront' - / = render partial: 'shared/components/filter_controls_shopfront' - - .row - .small-12.columns - %ul.small-block-grid-2.medium-block-grid-3.large-block-grid-4 - %taxon-selector{objects: "Products.products | products:query | products:showProfiles", - results: "activeTaxons"} + %li + %a + %span + + 7 more + %i.ofn-i_052-point-down + / TODO: Needs controller for Angular Bootstrap directive for dropdown: + / {"ng-controller" => "DropdownCtrl"} + + / %li + / .btn-group{:dropdown => "", "is-open" => "status.isopen"} + / %button.btn.btn-primary.dropdown-toggle{"dropdown-toggle" => "", "ng-disabled" => "disabled", :type => "button"} + / + 7 more + / %span.caret + / %ul.dropdown-menu{:role => "menu"} + / %li + / %a{:href => "#"} Action + / %li + / %a{:href => "#"} Another action + / %li + / %a{:href => "#"} Something else here + / %li.divider + / %li + / %a{:href => "#"} Separated link + +.filter-box.property-box-shopfront.animate-hide.text-right + %ul + %li + %a Organic Certified + %li + %a Free Range + %li + %a Biodynamic + %li + %a + + 2 more + %span.caret + + + \ No newline at end of file diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 48436881a7..a654c8db5e 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -1,6 +1,22 @@ %products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} + // TODO: Needs an ng-show to slide content down + .row.animate-hide + .small-12.columns + .alert-box.search-alert.ng-scope{"ng-show" => "visible", "ofn-inline-alert" => ""} + %a.right{"ng-click" => "close()"} + Clear all + %i.ofn-i_009-close + %span.filter-label + Showing: + %span.applied-properties + Certified Organic + %span.applied-taxons + Fruit & Vegetables & Dairy + with + %span.applied-search + "search query string" .row .small-12.medium-6.large-5.columns %input#search.text{"ng-model" => "query", From fc7abd5d02d6a0cd50d75f78bd654db6a3966d8b Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 25 Feb 2015 16:20:10 +1100 Subject: [PATCH 06/81] Changing order of tabs at top of shopfront, leaving more room for long shopfront names in about tab --- app/views/shopping_shared/_tabs.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/shopping_shared/_tabs.html.haml b/app/views/shopping_shared/_tabs.html.haml index d136efa045..b654366ecc 100644 --- a/app/views/shopping_shared/_tabs.html.haml +++ b/app/views/shopping_shared/_tabs.html.haml @@ -2,10 +2,10 @@ .row %tabset -# Build all tabs. - - for name, heading_cols in { about: ["About #{current_distributor.name}", 4], - producers: ["Producers",3], - groups: ["Groups",2], - contact: ["Contact",3]} + - for name, heading_cols in { about: ["About #{current_distributor.name}", 6], + producers: ["Producers",2], + contact: ["Contact",2], + groups: ["Groups",2]} -# tabs take tab path in 'active' and 'select' functions defined in TabsCtrl. - heading, cols = heading_cols %tab.columns{heading: heading, From 4afef8215a764c20d34c96febd32d34b64dc8e60 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 12:12:36 +1100 Subject: [PATCH 07/81] Basic implementation of single line selectors --- .../controllers/products_controller.js.coffee | 3 + .../directives/single_line_selector.coffee | 33 ++++++++ .../directives/taxon_selector.js.coffee | 12 ++- .../templates/taxon_selector.html.haml | 4 +- .../darkswarm/_shop-filters.css.sass | 17 ++-- app/views/shop/products/_filters.html.haml | 78 +++++++++---------- 6 files changed, 94 insertions(+), 53 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/directives/single_line_selector.coffee diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index 019a09e96f..e18aebb267 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -9,6 +9,9 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, $scope.limit = 3 $scope.order_cycle = OrderCycle.order_cycle + $scope.$watch "Products.loading", (newValue, oldValue) -> + $scope.$broadcast("loadTaxonSelectors") if !newValue + $scope.incrementLimit = -> if $scope.limit < Products.products.length $scope.limit = $scope.limit + 1 diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee new file mode 100644 index 0000000000..5b154c42ac --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee @@ -0,0 +1,33 @@ +Darkswarm.directive 'singleLineSelector', ($timeout) -> + restrict: 'E' + link: (scope,element,attrs) -> + scope.activeTaxons = [] + scope.taxonSelectors = [] + + # From: http://stackoverflow.com/questions/4298612/jquery-how-to-call-resize-event-only-once-its-finished-resizing + debouncer = (func, timeout) -> + timeoutID = undefined + timeout = timeout or 200 + -> + subject = this + args = arguments + clearTimeout timeoutID + timeoutID = setTimeout(-> + func.apply subject, Array::slice.call(args) + , timeout) + + fit = -> + used = $(element).find("li.more").outerWidth(true) + available = $(element).parent(".filter-box").innerWidth() + $(element).find("li").not(".more").each (i) -> + used += $(this).outerWidth(true) + scope.taxonSelectors[i].fits = used <= available + return null # So we don't exit the loop on false + + scope.$watchCollection "taxonSelectors", -> + selector.fits = true for selector in scope.taxonSelectors + $timeout fit, 0, true + + $(window).resize debouncer (e) -> + scope.$apply -> selector.fits = true for selector in scope.taxonSelectors + $timeout fit, 0, true diff --git a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee index 633ecd22f4..5fb67c8405 100644 --- a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee @@ -5,7 +5,8 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> replace: true scope: objects: "&" - results: "=" + activeTaxons: "=" + taxonSelectors: "=" templateUrl: "taxon_selector.html" link: (scope, elem, attr)-> @@ -13,14 +14,17 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> selectors = null # To get scoping/closure right scope.emit = -> - scope.results = selectors.filter (selector)-> + scope.activeTaxons = selectors.filter (selector)-> selector.active .map (selector)-> selector.taxon.id + scope.$on 'loadTaxonSelectors', -> + scope.taxonSelectors = scope.selectors() + # Build hash of unique taxons, each of which gets an ActiveSelector scope.selectors = -> - taxons = {} + taxons = {} selectors = [] for object in scope.objects() for taxon in object.taxons @@ -28,7 +32,7 @@ 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 diff --git a/app/assets/javascripts/templates/taxon_selector.html.haml b/app/assets/javascripts/templates/taxon_selector.html.haml index 02450c8d12..513225d6f4 100644 --- a/app/assets/javascripts/templates/taxon_selector.html.haml +++ b/app/assets/javascripts/templates/taxon_selector.html.haml @@ -1,3 +1,3 @@ -%active-selector{"ng-repeat" => "selector in selectors()"} - %render-svg{path: "{{selector.taxon.icon}}"} +%active-selector{"ng-repeat" => "selector in taxonSelectors", "ng-show" => "selector.fits"} + %render-svg{path: "{{selector.taxon.icon}}"} %span {{ selector.taxon.name }} diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass index fbebe3a431..a714c4a361 100644 --- a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -1,7 +1,7 @@ @import mixins @import branding @import big-input -@import animations +@import animations // Alert when search, taxon, filter is triggered @@ -27,6 +27,10 @@ .filter-box.filter-box-shopfront, .filter-box.property-box-shopfront background: transparent + single-line-selector + overflow-x: hidden + white-space: nowrap + ul margin: 0 ul, ul li @@ -35,11 +39,11 @@ display: inline-block @include border-radius(0) padding: 0 - margin: 0 0 0.25rem 0.25rem + margin: 0 0 0.25rem 0.25rem &:hover, &:focus background: transparent - - li.active + + li.active @include box-shadow(none) a, a:hover, a:focus, a:active, a.active border: 1px solid $clr-blue @@ -52,7 +56,7 @@ li a @include border-radius(0.5em) border: 1px solid $clr-blue-light - padding: 0.625em + padding: 0.625em font-size: 0.875em color: $clr-blue font-size: 0.75em @@ -77,7 +81,7 @@ // Shopfront properties .filter-box.property-box-shopfront - li.active + li.active @include box-shadow(none) a, a:hover, a:focus, a:active, a.active border: 1px solid #333 @@ -89,4 +93,3 @@ &:hover, &:focus border: 1px solid #999 color: #666 - diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 61f807c684..6b8a9de1a7 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,45 +1,43 @@ .filter-box.filter-box-shopfront.animate-hide.text-right - %ul - %taxon-selector{objects: "Products.products | products:query | products:showProfiles", - results: "activeTaxons"} + %single-line-selector + %ul + %taxon-selector{objects: "Products.products | products:query | products:showProfiles", + "active-taxons" => "activeTaxons", "taxon-selectors" => "taxonSelectors" } - %li - %a - %span - + 7 more - %i.ofn-i_052-point-down + %li.more + %a + %span + + {{ (taxonSelectors | filter:{ fits: false }).length }} more + %i.ofn-i_052-point-down - / TODO: Needs controller for Angular Bootstrap directive for dropdown: - / {"ng-controller" => "DropdownCtrl"} - - / %li - / .btn-group{:dropdown => "", "is-open" => "status.isopen"} - / %button.btn.btn-primary.dropdown-toggle{"dropdown-toggle" => "", "ng-disabled" => "disabled", :type => "button"} - / + 7 more - / %span.caret - / %ul.dropdown-menu{:role => "menu"} - / %li - / %a{:href => "#"} Action - / %li - / %a{:href => "#"} Another action - / %li - / %a{:href => "#"} Something else here - / %li.divider - / %li - / %a{:href => "#"} Separated link + / TODO: Needs controller for Angular Bootstrap directive for dropdown: + / {"ng-controller" => "DropdownCtrl"} -.filter-box.property-box-shopfront.animate-hide.text-right - %ul - %li - %a Organic Certified - %li - %a Free Range - %li - %a Biodynamic - %li - %a - + 2 more - %span.caret + / %li + / .btn-group{:dropdown => "", "is-open" => "status.isopen"} + / %button.btn.btn-primary.dropdown-toggle{"dropdown-toggle" => "", "ng-disabled" => "disabled", :type => "button"} + / + 7 more + / %span.caret + / %ul.dropdown-menu{:role => "menu"} + / %li + / %a{:href => "#"} Action + / %li + / %a{:href => "#"} Another action + / %li + / %a{:href => "#"} Something else here + / %li.divider + / %li + / %a{:href => "#"} Separated link - - \ No newline at end of file +-# .filter-box.property-box-shopfront.animate-hide.text-right +-# %ul +-# %li +-# %a Organic Certified +-# %li +-# %a Free Range +-# %li +-# %a Biodynamic +-# %li +-# %a +-# + 2 more +-# %span.caret From 5bddada013a491c075873bd9b964c78563388c44 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 12:18:34 +1100 Subject: [PATCH 08/81] Removing unnecessary setter --- .../darkswarm/directives/single_line_selector.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee index 5b154c42ac..ab7d292c33 100644 --- a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee +++ b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee @@ -1,9 +1,6 @@ Darkswarm.directive 'singleLineSelector', ($timeout) -> restrict: 'E' link: (scope,element,attrs) -> - scope.activeTaxons = [] - scope.taxonSelectors = [] - # From: http://stackoverflow.com/questions/4298612/jquery-how-to-call-resize-event-only-once-its-finished-resizing debouncer = (func, timeout) -> timeoutID = undefined From ed94cf57d3f02d423ccf01def05bef23ffe513bd Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 15:10:42 +1100 Subject: [PATCH 09/81] Adding the 'more' box for single-line-selectors --- .../controllers/products_controller.js.coffee | 7 ++++- .../directives/single_line_selector.coffee | 28 ++++++++++++++++--- app/views/shop/products/_filters.html.haml | 10 +++++-- app/views/shop/products/_form.html.haml | 13 ++++----- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index e18aebb267..23c05a1de5 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, FilterSelectorsService, Cart) -> +Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, FilterSelectorsService, Cart, Taxons) -> $scope.Products = Products $scope.Cart = Cart $scope.totalActive = FilterSelectorsService.totalActive @@ -20,3 +20,8 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, code = e.keyCode || e.which if code == 13 e.preventDefault() + + $scope.appliedTaxonsList = () -> + $scope.activeTaxons.map( (taxon_id) -> + Taxons.taxons_by_id[taxon_id].name + ).join(" & ") if $scope.activeTaxons? diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee index ab7d292c33..b4e94d8b9e 100644 --- a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee +++ b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee @@ -1,10 +1,26 @@ -Darkswarm.directive 'singleLineSelector', ($timeout) -> +Darkswarm.directive 'singleLineSelector', ($timeout, $filter) -> restrict: 'E' link: (scope,element,attrs) -> + scope.fitting = false + + scope.overFlowSelectors = -> + return [] unless scope.taxonSelectors? + $filter('filter')(scope.taxonSelectors, { fits: false }) + + scope.selectedOverFlowSelectors = -> + $filter('filter')(scope.overFlowSelectors(), { active: true }) + + # had to duplicate this to make overflow selectors work + scope.emit = -> + scope.activeTaxons = scope.taxonSelectors.filter (selector)-> + selector.active + .map (selector)-> + selector.taxon.id + # From: http://stackoverflow.com/questions/4298612/jquery-how-to-call-resize-event-only-once-its-finished-resizing debouncer = (func, timeout) -> timeoutID = undefined - timeout = timeout or 200 + timeout = timeout or 50 -> subject = this args = arguments @@ -20,11 +36,15 @@ Darkswarm.directive 'singleLineSelector', ($timeout) -> used += $(this).outerWidth(true) scope.taxonSelectors[i].fits = used <= available return null # So we don't exit the loop on false + scope.fitting = false scope.$watchCollection "taxonSelectors", -> - selector.fits = true for selector in scope.taxonSelectors - $timeout fit, 0, true + if scope.taxonSelectors? + scope.fitting = true + selector.fits = true for selector in scope.taxonSelectors + $timeout fit, 0, true $(window).resize debouncer (e) -> + scope.fitting = true scope.$apply -> selector.fits = true for selector in scope.taxonSelectors $timeout fit, 0, true diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 6b8a9de1a7..3aca51e013 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -4,11 +4,15 @@ %taxon-selector{objects: "Products.products | products:query | products:showProfiles", "active-taxons" => "activeTaxons", "taxon-selectors" => "taxonSelectors" } - %li.more - %a + %li.more{ ng: { show: "overFlowSelectors().length > 0 || fitting", class: "{active: selectedOverFlowSelectors().length > 0}" } } + %a.dropdown{ dropdown: { toggle: "#more-taxons" } } %span - + {{ (taxonSelectors | filter:{ fits: false }).length }} more + + {{ overFlowSelectors().length }} more %i.ofn-i_052-point-down + %div.f-dropdown#more-taxons + %active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } } + %render-svg{path: "{{selector.taxon.icon}}"} + %span {{ selector.taxon.name }} / TODO: Needs controller for Angular Bootstrap directive for dropdown: / {"ng-controller" => "DropdownCtrl"} diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index a654c8db5e..45a8002270 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -5,18 +5,17 @@ .row.animate-hide .small-12.columns .alert-box.search-alert.ng-scope{"ng-show" => "visible", "ofn-inline-alert" => ""} - %a.right{"ng-click" => "close()"} - Clear all + %a.right{"ng-click" => "close()"} + Clear all %i.ofn-i_009-close %span.filter-label Showing: %span.applied-properties Certified Organic %span.applied-taxons - Fruit & Vegetables & Dairy - with - %span.applied-search - "search query string" + {{ appliedTaxonsList() }} + %span.applied-search{ ng: { hide: "!query"} } + with {{ query }} .row .small-12.medium-6.large-5.columns %input#search.text{"ng-model" => "query", @@ -30,7 +29,6 @@ %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons) track by product.id ", "id" => "product-{{ product.id }}"} - = render partial: "shop/products/summary" %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"} @@ -55,4 +53,3 @@ %form{action: cart_path} %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } - From 24b40182b58117ee068a9894c8e2e1c1e8167200 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 15:34:27 +1100 Subject: [PATCH 10/81] clearAll for product page also clears any text search filter --- .../darkswarm/controllers/products_controller.js.coffee | 4 ++++ app/views/shop/products/_form.html.haml | 2 +- 2 files changed, 5 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 23c05a1de5..2d96397cfb 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -25,3 +25,7 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, $scope.activeTaxons.map( (taxon_id) -> Taxons.taxons_by_id[taxon_id].name ).join(" & ") if $scope.activeTaxons? + + $scope.clearAll = -> + $scope.query = "" + FilterSelectorsService.clearAll() diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 45a8002270..33d566ee1b 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -5,7 +5,7 @@ .row.animate-hide .small-12.columns .alert-box.search-alert.ng-scope{"ng-show" => "visible", "ofn-inline-alert" => ""} - %a.right{"ng-click" => "close()"} + %a.right{"ng-click" => "clearAll()"} Clear all %i.ofn-i_009-close %span.filter-label From 0142f9467aabcb247f3540b6349743769ede2de8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 16:00:53 +1100 Subject: [PATCH 11/81] Improve display of overflow selectors --- app/views/shop/products/_filters.html.haml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 3aca51e013..59bda1baf4 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -9,10 +9,11 @@ %span + {{ overFlowSelectors().length }} more %i.ofn-i_052-point-down - %div.f-dropdown#more-taxons - %active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } } - %render-svg{path: "{{selector.taxon.icon}}"} - %span {{ selector.taxon.name }} + .f-dropdown.content#more-taxons + %ul + %active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } } + %render-svg{path: "{{selector.taxon.icon}}"} + %span {{ selector.taxon.name }} / TODO: Needs controller for Angular Bootstrap directive for dropdown: / {"ng-controller" => "DropdownCtrl"} From e7854bcd8eb2e330ab0cbbc27ef8c1a318f6c6e0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 16:38:21 +1100 Subject: [PATCH 12/81] Moving single line selectors to template, making sure other users of taxons-selector still work --- .../directives/single_line_selector.coffee | 3 +- .../directives/taxon_selector.js.coffee | 6 ++++ .../templates/single_line_selectors.html.haml | 14 ++++++++ .../templates/taxon_selector.html.haml | 2 +- app/views/home/_filters.html.haml | 2 +- app/views/producers/_filters.html.haml | 5 ++- app/views/shop/products/_filters.html.haml | 35 +------------------ 7 files changed, 27 insertions(+), 40 deletions(-) create mode 100644 app/assets/javascripts/templates/single_line_selectors.html.haml diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee index b4e94d8b9e..de5abd5ab8 100644 --- a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee +++ b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee @@ -1,5 +1,6 @@ -Darkswarm.directive 'singleLineSelector', ($timeout, $filter) -> +Darkswarm.directive 'singleLineSelectors', ($timeout, $filter) -> restrict: 'E' + templateUrl: "single_line_selectors.html" link: (scope,element,attrs) -> scope.fitting = false diff --git a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee index 5fb67c8405..069964092a 100644 --- a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee @@ -45,3 +45,9 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> taxon: taxon selectors.push selector selectors + + scope.ifDefined = (value, if_undefined) -> + if angular.isDefined(value) + value + else + if_undefined diff --git a/app/assets/javascripts/templates/single_line_selectors.html.haml b/app/assets/javascripts/templates/single_line_selectors.html.haml new file mode 100644 index 0000000000..da48fd97e1 --- /dev/null +++ b/app/assets/javascripts/templates/single_line_selectors.html.haml @@ -0,0 +1,14 @@ +%ul + %taxon-selector{objects: "Products.products | products:query | products:showProfiles", + "active-taxons" => "activeTaxons", "taxon-selectors" => "taxonSelectors" } + + %li.more{ ng: { show: "overFlowSelectors().length > 0 || fitting", class: "{active: selectedOverFlowSelectors().length > 0}" } } + %a.dropdown{ dropdown: { toggle: "#more-taxons" } } + %span + + {{ overFlowSelectors().length }} more + %i.ofn-i_052-point-down + .f-dropdown.content#more-taxons + %ul + %active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } } + %render-svg{path: "{{selector.taxon.icon}}"} + %span {{ selector.taxon.name }} diff --git a/app/assets/javascripts/templates/taxon_selector.html.haml b/app/assets/javascripts/templates/taxon_selector.html.haml index 513225d6f4..7bd178c2ab 100644 --- a/app/assets/javascripts/templates/taxon_selector.html.haml +++ b/app/assets/javascripts/templates/taxon_selector.html.haml @@ -1,3 +1,3 @@ -%active-selector{"ng-repeat" => "selector in taxonSelectors", "ng-show" => "selector.fits"} +%active-selector{ ng: { repeat: "selector in selectors()", show: "ifDefined(selector.fits, true)" } } %render-svg{path: "{{selector.taxon.icon}}"} %span {{ selector.taxon.name }} diff --git a/app/views/home/_filters.html.haml b/app/views/home/_filters.html.haml index 52fceb6688..09d5e46df4 100644 --- a/app/views/home/_filters.html.haml +++ b/app/views/home/_filters.html.haml @@ -11,7 +11,7 @@ Type %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-5 %taxon-selector{objects: "Enterprises.hubs | searchEnterprises:query", - results: "activeTaxons"} + "active-taxons" => "activeTaxons"} .small-12.large-3.columns %h5.tdhead .light Filter by diff --git a/app/views/producers/_filters.html.haml b/app/views/producers/_filters.html.haml index e1cd0ccba9..0f863a7a2f 100644 --- a/app/views/producers/_filters.html.haml +++ b/app/views/producers/_filters.html.haml @@ -11,7 +11,6 @@ .light Filter by Type %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-6 - %taxon-selector{objects: "Enterprises.producers | searchEnterprises:query ", - results: "activeTaxons"} + %taxon-selector{objects: "Enterprises.producers | searchEnterprises:query ", + "active-taxons" => "activeTaxons"} = render partial: 'shared/components/filter_box' - diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 59bda1baf4..8581ea1607 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,38 +1,5 @@ .filter-box.filter-box-shopfront.animate-hide.text-right - %single-line-selector - %ul - %taxon-selector{objects: "Products.products | products:query | products:showProfiles", - "active-taxons" => "activeTaxons", "taxon-selectors" => "taxonSelectors" } - - %li.more{ ng: { show: "overFlowSelectors().length > 0 || fitting", class: "{active: selectedOverFlowSelectors().length > 0}" } } - %a.dropdown{ dropdown: { toggle: "#more-taxons" } } - %span - + {{ overFlowSelectors().length }} more - %i.ofn-i_052-point-down - .f-dropdown.content#more-taxons - %ul - %active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } } - %render-svg{path: "{{selector.taxon.icon}}"} - %span {{ selector.taxon.name }} - - / TODO: Needs controller for Angular Bootstrap directive for dropdown: - / {"ng-controller" => "DropdownCtrl"} - - / %li - / .btn-group{:dropdown => "", "is-open" => "status.isopen"} - / %button.btn.btn-primary.dropdown-toggle{"dropdown-toggle" => "", "ng-disabled" => "disabled", :type => "button"} - / + 7 more - / %span.caret - / %ul.dropdown-menu{:role => "menu"} - / %li - / %a{:href => "#"} Action - / %li - / %a{:href => "#"} Another action - / %li - / %a{:href => "#"} Something else here - / %li.divider - / %li - / %a{:href => "#"} Separated link + %single-line-selectors -# .filter-box.property-box-shopfront.animate-hide.text-right -# %ul From cb623b75c1e069a3d17f06c737608a5faa4759dc Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 18:15:14 +1100 Subject: [PATCH 13/81] Generalising taxons-selector -> filter-selector --- .../controllers/products_controller.js.coffee | 2 +- .../directives/single_line_selector.coffee | 21 ++++++----- .../directives/taxon_selector.js.coffee | 36 ++++++++----------- .../darkswarm/filters/taxons_of.js.coffee | 10 ++++++ ...or.html.haml => filter_selector.html.haml} | 4 +-- .../templates/single_line_selectors.html.haml | 12 +++---- app/views/shop/products/_filters.html.haml | 2 +- 7 files changed, 47 insertions(+), 40 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/filters/taxons_of.js.coffee rename app/assets/javascripts/templates/{taxon_selector.html.haml => filter_selector.html.haml} (55%) diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index 2d96397cfb..458e500d99 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -10,7 +10,7 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, $scope.order_cycle = OrderCycle.order_cycle $scope.$watch "Products.loading", (newValue, oldValue) -> - $scope.$broadcast("loadTaxonSelectors") if !newValue + $scope.$broadcast("loadFilterSelectors") if !newValue $scope.incrementLimit = -> if $scope.limit < Products.products.length diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee index de5abd5ab8..53ef2f9e9e 100644 --- a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee +++ b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee @@ -1,22 +1,25 @@ Darkswarm.directive 'singleLineSelectors', ($timeout, $filter) -> restrict: 'E' templateUrl: "single_line_selectors.html" + scope: + objects: "&" + activeSelectors: "=" link: (scope,element,attrs) -> scope.fitting = false scope.overFlowSelectors = -> - return [] unless scope.taxonSelectors? - $filter('filter')(scope.taxonSelectors, { fits: false }) + return [] unless scope.allSelectors? + $filter('filter')(scope.allSelectors, { fits: false }) scope.selectedOverFlowSelectors = -> $filter('filter')(scope.overFlowSelectors(), { active: true }) # had to duplicate this to make overflow selectors work scope.emit = -> - scope.activeTaxons = scope.taxonSelectors.filter (selector)-> + scope.activeSelectors = scope.allSelectors.filter (selector)-> selector.active .map (selector)-> - selector.taxon.id + selector.object.id # From: http://stackoverflow.com/questions/4298612/jquery-how-to-call-resize-event-only-once-its-finished-resizing debouncer = (func, timeout) -> @@ -35,17 +38,17 @@ Darkswarm.directive 'singleLineSelectors', ($timeout, $filter) -> available = $(element).parent(".filter-box").innerWidth() $(element).find("li").not(".more").each (i) -> used += $(this).outerWidth(true) - scope.taxonSelectors[i].fits = used <= available + scope.allSelectors[i].fits = used <= available return null # So we don't exit the loop on false scope.fitting = false - scope.$watchCollection "taxonSelectors", -> - if scope.taxonSelectors? + scope.$watchCollection "allSelectors", -> + if scope.allSelectors? scope.fitting = true - selector.fits = true for selector in scope.taxonSelectors + selector.fits = true for selector in scope.allSelectors $timeout fit, 0, true $(window).resize debouncer (e) -> scope.fitting = true - scope.$apply -> selector.fits = true for selector in scope.taxonSelectors + scope.$apply -> selector.fits = true for selector in scope.allSelectors $timeout fit, 0, true diff --git a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee index 069964092a..b6f7d53552 100644 --- a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee @@ -1,48 +1,42 @@ -Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> +Darkswarm.directive "filterSelector", (FilterSelectorsService)-> # Automatically builds activeSelectors for taxons # Lots of magic here restrict: 'E' replace: true scope: objects: "&" - activeTaxons: "=" - taxonSelectors: "=" - templateUrl: "taxon_selector.html" + activeSelectors: "=" + allSelectors: "=" + templateUrl: "filter_selector.html" link: (scope, elem, attr)-> selectors_by_id = {} selectors = null # To get scoping/closure right scope.emit = -> - scope.activeTaxons = selectors.filter (selector)-> + scope.activeSelectors = selectors.filter (selector)-> selector.active .map (selector)-> - selector.taxon.id + selector.object.id - scope.$on 'loadTaxonSelectors', -> - scope.taxonSelectors = scope.selectors() + # This can be called from a parent controller + # when data has been loaded + scope.$on 'loadFilterSelectors', -> + scope.allSelectors = scope.selectors() - # Build hash of unique taxons, each of which gets an ActiveSelector + # Build a list of selectors scope.selectors = -> - taxons = {} - selectors = [] - for object in scope.objects() - for taxon in object.taxons - taxons[taxon.id] = taxon - if object.supplied_taxons - for taxon in object.supplied_taxons - taxons[taxon.id] = taxon - - # Generate a selector for each taxon. + # Generate a selector for each object. # 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 + selectors = [] + for id, object of scope.objects() if selector = selectors_by_id[id] selectors.push selector else selector = selectors_by_id[id] = FilterSelectorsService.new - taxon: taxon + object: object selectors.push selector selectors diff --git a/app/assets/javascripts/darkswarm/filters/taxons_of.js.coffee b/app/assets/javascripts/darkswarm/filters/taxons_of.js.coffee new file mode 100644 index 0000000000..6726a5c22e --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/taxons_of.js.coffee @@ -0,0 +1,10 @@ +Darkswarm.filter 'taxonsOf', -> + (objects)-> + taxons = {} + for object in objects + for taxon in object.taxons + taxons[taxon.id] = taxon + if object.supplied_taxons + for taxon in object.supplied_taxons + taxons[taxon.id] = taxon + taxons diff --git a/app/assets/javascripts/templates/taxon_selector.html.haml b/app/assets/javascripts/templates/filter_selector.html.haml similarity index 55% rename from app/assets/javascripts/templates/taxon_selector.html.haml rename to app/assets/javascripts/templates/filter_selector.html.haml index 7bd178c2ab..576bfdf89c 100644 --- a/app/assets/javascripts/templates/taxon_selector.html.haml +++ b/app/assets/javascripts/templates/filter_selector.html.haml @@ -1,3 +1,3 @@ %active-selector{ ng: { repeat: "selector in selectors()", show: "ifDefined(selector.fits, true)" } } - %render-svg{path: "{{selector.taxon.icon}}"} - %span {{ selector.taxon.name }} + %render-svg{path: "{{selector.object.icon}}"} + %span {{ selector.object.name }} diff --git a/app/assets/javascripts/templates/single_line_selectors.html.haml b/app/assets/javascripts/templates/single_line_selectors.html.haml index da48fd97e1..5be7e4f7bc 100644 --- a/app/assets/javascripts/templates/single_line_selectors.html.haml +++ b/app/assets/javascripts/templates/single_line_selectors.html.haml @@ -1,14 +1,14 @@ %ul - %taxon-selector{objects: "Products.products | products:query | products:showProfiles", - "active-taxons" => "activeTaxons", "taxon-selectors" => "taxonSelectors" } + -# In order for the single-line-selector scope to have access to the available selectors, + %filter-selector{objects: "objects()", "active-selectors" => "activeSelectors", "all-selectors" => "allSelectors" } %li.more{ ng: { show: "overFlowSelectors().length > 0 || fitting", class: "{active: selectedOverFlowSelectors().length > 0}" } } - %a.dropdown{ dropdown: { toggle: "#more-taxons" } } + %a.dropdown{ dropdown: { toggle: "#show-more" } } %span + {{ overFlowSelectors().length }} more %i.ofn-i_052-point-down - .f-dropdown.content#more-taxons + .f-dropdown.content#show-more %ul %active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } } - %render-svg{path: "{{selector.taxon.icon}}"} - %span {{ selector.taxon.name }} + %render-svg{path: "{{selector.object.icon}}"} + %span {{ selector.object.name }} diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 8581ea1607..3cb72a4853 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,5 +1,5 @@ .filter-box.filter-box-shopfront.animate-hide.text-right - %single-line-selectors + %single-line-selectors{ objects: "Products.products | products:query | taxonsOf", "active-selectors" => "activeTaxons"} -# .filter-box.property-box-shopfront.animate-hide.text-right -# %ul From c992937608bb4db4b426f2c150e62edc694d478b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 18:28:49 +1100 Subject: [PATCH 14/81] Moving taxon-selector directive to correct location --- .../{taxon_selector.js.coffee => filter_selector.js.coffee} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/darkswarm/directives/{taxon_selector.js.coffee => filter_selector.js.coffee} (100%) diff --git a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee similarity index 100% rename from app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee rename to app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee From 067b814daaf081411ad4246fd5ecf20a90d86f1b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 18:29:12 +1100 Subject: [PATCH 15/81] Making legacy filters work properly with filter-selector directive --- app/views/home/_filters.html.haml | 3 +-- app/views/producers/_filters.html.haml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/home/_filters.html.haml b/app/views/home/_filters.html.haml index 09d5e46df4..7fcda4bbf4 100644 --- a/app/views/home/_filters.html.haml +++ b/app/views/home/_filters.html.haml @@ -10,8 +10,7 @@ .light Filter by Type %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-5 - %taxon-selector{objects: "Enterprises.hubs | searchEnterprises:query", - "active-taxons" => "activeTaxons"} + %filter-selector{objects: "Enterprises.hubs | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"} .small-12.large-3.columns %h5.tdhead .light Filter by diff --git a/app/views/producers/_filters.html.haml b/app/views/producers/_filters.html.haml index 0f863a7a2f..b548bd90c4 100644 --- a/app/views/producers/_filters.html.haml +++ b/app/views/producers/_filters.html.haml @@ -11,6 +11,5 @@ .light Filter by Type %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-6 - %taxon-selector{objects: "Enterprises.producers | searchEnterprises:query ", - "active-taxons" => "activeTaxons"} + %filter-selector{objects: "Enterprises.producers | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"} = render partial: 'shared/components/filter_box' From 55b8918ea13212daa30b47f953656d291f18165f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 Mar 2015 18:38:01 +1100 Subject: [PATCH 16/81] Updating comment --- .../darkswarm/directives/filter_selector.js.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee index b6f7d53552..c904a16b8d 100644 --- a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee @@ -19,8 +19,9 @@ Darkswarm.directive "filterSelector", (FilterSelectorsService)-> .map (selector)-> selector.object.id - # This can be called from a parent controller - # when data has been loaded + # This can be called from a parent scope + # when data has been loaded, in order to pass + # selectors up scope.$on 'loadFilterSelectors', -> scope.allSelectors = scope.selectors() From 06f10398da77a3c19ab2f5c9a53ca442b1b7713f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 6 Mar 2015 11:22:52 +1100 Subject: [PATCH 17/81] Adding product property filter to shop page --- .../darkswarm/filters/properties_of.js.coffee | 7 ++++ .../darkswarm/services/products.js.coffee | 15 ++++---- .../darkswarm/services/properties.js.coffee | 9 +++++ app/helpers/injection_helper.rb | 4 +++ app/serializers/api/id_name_serializer.rb | 3 ++ app/serializers/api/product_serializer.rb | 2 +- app/views/layouts/darkswarm.html.haml | 3 +- app/views/shop/products/_filters.html.haml | 14 ++------ .../consumer/shopping/shopping_spec.rb | 33 +++++++++-------- .../products_controller_spec.js.coffee | 14 +++++--- ...spec.js.coffee => products_spec.js.coffee} | 35 +++++++++++++++---- .../services/properties_spec.js.coffee | 16 +++++++++ 12 files changed, 105 insertions(+), 50 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/filters/properties_of.js.coffee create mode 100644 app/assets/javascripts/darkswarm/services/properties.js.coffee create mode 100644 app/serializers/api/id_name_serializer.rb rename spec/javascripts/unit/darkswarm/services/{product_spec.js.coffee => products_spec.js.coffee} (74%) create mode 100644 spec/javascripts/unit/darkswarm/services/properties_spec.js.coffee diff --git a/app/assets/javascripts/darkswarm/filters/properties_of.js.coffee b/app/assets/javascripts/darkswarm/filters/properties_of.js.coffee new file mode 100644 index 0000000000..4ab8c94bfd --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/properties_of.js.coffee @@ -0,0 +1,7 @@ +Darkswarm.filter 'propertiesOf', -> + (objects)-> + properties = {} + for object in objects + for property in object.properties + properties[property.id] = property + properties diff --git a/app/assets/javascripts/darkswarm/services/products.js.coffee b/app/assets/javascripts/darkswarm/services/products.js.coffee index 9da3187f82..db8bbbb7ee 100644 --- a/app/assets/javascripts/darkswarm/services/products.js.coffee +++ b/app/assets/javascripts/darkswarm/services/products.js.coffee @@ -1,27 +1,28 @@ -Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Cart, Variants) -> +Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Properties, Cart, Variants) -> new class Products constructor: -> @update() - + # TODO: don't need to scope this into object # Already on object as far as controller scope is concerned products: null loading: true update: => - @loading = true + @loading = true @products = $resource("/shop/products").query (products)=> @extend() && @dereference() - @registerVariants() + @registerVariants() @registerVariantsWithCart() @loading = false @ - + dereference: -> for product in @products product.supplier = Enterprises.enterprises_by_id[product.supplier.id] Dereferencer.dereference product.taxons, Taxons.taxons_by_id - + Dereferencer.dereference product.properties, Properties.properties_by_id + # May return different objects! If the variant has already been registered # by another service, we fetch those registerVariants: -> @@ -45,7 +46,7 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Car prices = (v.price for v in product.variants) product.price = Math.min.apply(null, prices) product.hasVariants = product.variants?.length > 0 - + 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/darkswarm/services/properties.js.coffee b/app/assets/javascripts/darkswarm/services/properties.js.coffee new file mode 100644 index 0000000000..37314947b4 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/properties.js.coffee @@ -0,0 +1,9 @@ +Darkswarm.factory "Properties", (properties)-> + new class Properties + # Populate ProductProperties.properties from json in page. + properties: properties + properties_by_id: {} + constructor: -> + # Map properties to id/object pairs for lookup. + for property in @properties + @properties_by_id[property.id] = property diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index a168d0284d..37794cef9d 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -21,6 +21,10 @@ module InjectionHelper inject_json_ams "taxons", Spree::Taxon.all, Api::TaxonSerializer end + def inject_properties + inject_json_ams "properties", Spree::Property.all, Api::IdNameSerializer + end + def inject_currency_config inject_json_ams "currencyConfig", {}, Api::CurrencyConfigSerializer end diff --git a/app/serializers/api/id_name_serializer.rb b/app/serializers/api/id_name_serializer.rb new file mode 100644 index 0000000000..1db5f2439e --- /dev/null +++ b/app/serializers/api/id_name_serializer.rb @@ -0,0 +1,3 @@ +class Api::IdNameSerializer < ActiveModel::Serializer + attributes :id, :name +end diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index 0026290b6d..78f7593e4f 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -35,7 +35,7 @@ class Api::CachedProductSerializer < ActiveModel::Serializer has_many :variants, serializer: Api::VariantSerializer has_many :taxons, serializer: Api::IdSerializer - has_many :properties, serializer: Api::PropertySerializer + has_many :properties, serializer: Api::IdSerializer has_many :images, serializer: Api::ImageSerializer has_one :supplier, serializer: Api::IdSerializer diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 0c6ec608d4..e1976ab167 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -28,6 +28,7 @@ = inject_json "user", "current_user" = inject_json "railsFlash", "flash" = inject_taxons + = inject_properties = inject_current_order = inject_currency_config @@ -37,6 +38,6 @@ %section{ role: "main" } = yield - + #footer %loading diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 3cb72a4853..9a347ddee0 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,15 +1,5 @@ .filter-box.filter-box-shopfront.animate-hide.text-right %single-line-selectors{ objects: "Products.products | products:query | taxonsOf", "active-selectors" => "activeTaxons"} --# .filter-box.property-box-shopfront.animate-hide.text-right --# %ul --# %li --# %a Organic Certified --# %li --# %a Free Range --# %li --# %a Biodynamic --# %li --# %a --# + 2 more --# %span.caret +.filter-box.property-box-shopfront.animate-hide.text-right + %single-line-selectors{ objects: "Products.products | products:query | propertiesOf", "active-selectors" => "activeProperties"} diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index a47b73ba6c..d540280656 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -15,7 +15,7 @@ feature "As a consumer I want to shop with a distributor", js: true do let(:product) { create(:simple_product, supplier: supplier) } let(:order) { create(:order, distributor: distributor) } - before do + before do set_order order end @@ -28,23 +28,23 @@ feature "As a consumer I want to shop with a distributor", js: true do visit shop_path page.should have_text distributor.name find("#tab_about a").click - first("distributor img")['src'].should == distributor.logo.url(:thumb) + first("distributor img")['src'].should == distributor.logo.url(:thumb) end it "shows the producers for a distributor" do - exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) + exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) exchange.variants << product.master visit shop_path find("#tab_producers a").click - page.should have_content supplier.name + page.should have_content supplier.name end describe "selecting an order cycle" do let(:exchange1) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } it "selects an order cycle if only one is open" do - exchange1.update_attribute :pickup_time, "turtles" + exchange1.update_attribute :pickup_time, "turtles" visit shop_path page.should have_selector "option[selected]", text: 'turtles' end @@ -52,8 +52,8 @@ feature "As a consumer I want to shop with a distributor", js: true do describe "with multiple order cycles" do let(:exchange2) { Exchange.find(oc2.exchanges.to_enterprises(distributor).outgoing.first.id) } before do - exchange1.update_attribute :pickup_time, "frogs" - exchange2.update_attribute :pickup_time, "turtles" + exchange1.update_attribute :pickup_time, "frogs" + exchange2.update_attribute :pickup_time, "turtles" end it "shows a select with all order cycles, but doesn't show the products by default" do @@ -62,20 +62,20 @@ feature "As a consumer I want to shop with a distributor", js: true do page.should have_selector "option", text: 'turtles' page.should_not have_selector("input.button.right", visible: true) end - + it "shows products after selecting an order cycle" do product.master.update_attribute(:display_name, "kitten") product.master.update_attribute(:display_as, "rabbit") exchange1.variants << product.master ## add product to exchange visit shop_path - page.should_not have_content product.name + page.should_not have_content product.name Spree::Order.last.order_cycle.should == nil select "frogs", :from => "order_cycle_id" page.should have_selector "products" - page.should have_content "Next order closing in 2 days" + page.should have_content "Next order closing in 2 days" Spree::Order.last.order_cycle.should == oc1 - page.should have_content product.name + page.should have_content product.name page.should have_content product.master.display_name page.should have_content product.master.display_as @@ -88,10 +88,10 @@ feature "As a consumer I want to shop with a distributor", js: true do describe "after selecting an order cycle with products visible" do let(:variant1) { create(:variant, product: product, price: 20) } let(:variant2) { create(:variant, product: product, price: 30) } - let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } + let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } before do - exchange.update_attribute :pickup_time, "frogs" + exchange.update_attribute :pickup_time, "frogs" exchange.variants << product.master exchange.variants << variant1 exchange.variants << variant2 @@ -134,7 +134,7 @@ feature "As a consumer I want to shop with a distributor", js: true do set_order_cycle(order, oc1) visit shop_path end - + 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 @@ -145,7 +145,7 @@ feature "As a consumer I want to shop with a distributor", js: true do li.max_quantity.should == 9 li.quantity.should == 5 end - + # TODO move to controller test pending "adding a product with a max quantity less than quantity results in max_quantity==quantity" do fill_in "variants[#{product.master.id}]", with: 5 @@ -165,7 +165,7 @@ feature "As a consumer I want to shop with a distributor", js: true do set_order_cycle(order, oc1) visit shop_path end - + it "should save group buy data to the cart" do fill_in "variants[#{variant.id}]", with: 6 fill_in "variant_attributes[#{variant.id}][max_quantity]", with: 7 @@ -213,4 +213,3 @@ feature "As a consumer I want to shop with a distributor", js: true do end end end - 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 73ecd611ef..67aced98c6 100644 --- a/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee @@ -4,19 +4,23 @@ describe 'ProductsCtrl', -> event = null Products = null Cart = {} + Taxons = null beforeEach -> module('Darkswarm') - Products = + Products = all: -> update: -> products: ["testy mctest"] + loading: false OrderCycle = order_cycle: {} - - inject ($controller) -> - scope = {} - ctrl = $controller 'ProductsCtrl', {$scope: scope, Products: Products, OrderCycle: OrderCycle, Cart: Cart} + Taxons: + taxons: [] + + inject ($rootScope, $controller) -> + scope = $rootScope + ctrl = $controller 'ProductsCtrl', {$scope: scope, Products: Products, OrderCycle: OrderCycle, Cart: Cart, Taxons: Taxons} it 'fetches products from Products', -> expect(scope.Products.products).toEqual ['testy mctest'] diff --git a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/products_spec.js.coffee similarity index 74% rename from spec/javascripts/unit/darkswarm/services/product_spec.js.coffee rename to spec/javascripts/unit/darkswarm/services/products_spec.js.coffee index 27e3f5aff6..2cb23522e2 100644 --- a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/products_spec.js.coffee @@ -4,13 +4,15 @@ describe 'Products service', -> Enterprises = null Variants = null Cart = null - CurrentHubMock = {} + CurrentHubMock = {} currentOrder = null product = null productWithImage = null + properties = null + taxons = null beforeEach -> - product = + product = test: "cats" supplier: id: 9 @@ -27,16 +29,23 @@ describe 'Products service', -> ] currentOrder = line_items: [] + properties = + { id: 1, name: "some property" } + taxons = + { id: 2, name: "some taxon" } module 'Darkswarm' module ($provide)-> - $provide.value "CurrentHub", CurrentHubMock - $provide.value "currentOrder", currentOrder + $provide.value "CurrentHub", CurrentHubMock + $provide.value "currentOrder", currentOrder + $provide.value "taxons", taxons + $provide.value "properties", properties null inject ($injector, _$httpBackend_)-> Products = $injector.get("Products") Enterprises = $injector.get("Enterprises") + Properties = $injector.get("Properties") Variants = $injector.get("Variants") Cart = $injector.get("Cart") $httpBackend = _$httpBackend_ @@ -44,20 +53,32 @@ describe 'Products service', -> it "Fetches products from the backend on init", -> $httpBackend.expectGET("/shop/products").respond([product]) $httpBackend.flush() - expect(Products.products[0].test).toEqual "cats" + expect(Products.products[0].test).toEqual "cats" it "dereferences suppliers", -> - Enterprises.enterprises_by_id = + Enterprises.enterprises_by_id = {id: 9, name: "test"} $httpBackend.expectGET("/shop/products").respond([{supplier : {id: 9}, master: {}}]) $httpBackend.flush() expect(Products.products[0].supplier).toBe Enterprises.enterprises_by_id["9"] + it "dereferences taxons", -> + product.taxons = [2] + $httpBackend.expectGET("/shop/products").respond([product]) + $httpBackend.flush() + expect(Products.products[0].taxons[1]).toBe taxons[0] + + it "dereferences properties", -> + product.properties = [1] + $httpBackend.expectGET("/shop/products").respond([product]) + $httpBackend.flush() + expect(Products.products[0].properties[1]).toBe properties[0] + it "registers variants with Variants service", -> product.variants = [{id: 1}] $httpBackend.expectGET("/shop/products").respond([product]) $httpBackend.flush() - expect(Products.products[0].variants[0]).toBe Variants.variants[1] + expect(Products.products[0].variants[0]).toBe Variants.variants[1] it "registers variants with the Cart", -> product.variants = [{id: 8}] diff --git a/spec/javascripts/unit/darkswarm/services/properties_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/properties_spec.js.coffee new file mode 100644 index 0000000000..d40d4f8901 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/properties_spec.js.coffee @@ -0,0 +1,16 @@ +describe "Properties service", -> + Properties = null + properties = [ + {id: 1, name: "Property1"} + {id: 2, name: "Property2"} + ] + + beforeEach -> + module('Darkswarm') + angular.module('Darkswarm').value 'properties', properties + + inject ($injector)-> + Properties = $injector.get("Properties") + + it "caches properties in an id-referenced hash", -> + expect(Properties.properties_by_id[1]).toBe properties[0] From e948bf1591bb0a35e63e72b5f4e8ce8d85f1436d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 6 Mar 2015 11:32:01 +1100 Subject: [PATCH 18/81] Only render selector icons if the icon exists --- app/assets/javascripts/templates/filter_selector.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/templates/filter_selector.html.haml b/app/assets/javascripts/templates/filter_selector.html.haml index 576bfdf89c..cddcd2f523 100644 --- a/app/assets/javascripts/templates/filter_selector.html.haml +++ b/app/assets/javascripts/templates/filter_selector.html.haml @@ -1,3 +1,3 @@ %active-selector{ ng: { repeat: "selector in selectors()", show: "ifDefined(selector.fits, true)" } } - %render-svg{path: "{{selector.object.icon}}"} + %render-svg{path: "{{selector.object.icon}}", ng: { if: "selector.object.icon"} } %span {{ selector.object.name }} From 578475a403d856aa0701b60c8db568267de04aab Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 6 Mar 2015 16:14:11 +1100 Subject: [PATCH 19/81] Filter products list on shop by active properties --- .../controllers/products_controller.js.coffee | 9 +++++++-- .../darkswarm/filters/properties.js.coffee | 16 ++++++++++++++++ .../templates/single_line_selectors.html.haml | 2 +- .../stylesheets/darkswarm/_shop-filters.css.sass | 6 +++++- app/views/shop/products/_form.html.haml | 6 ++++-- 5 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/filters/properties.js.coffee diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index 458e500d99..8443301765 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, FilterSelectorsService, Cart, Taxons) -> +Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, FilterSelectorsService, Cart, Taxons, Properties) -> $scope.Products = Products $scope.Cart = Cart $scope.totalActive = FilterSelectorsService.totalActive @@ -21,11 +21,16 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, if code == 13 e.preventDefault() - $scope.appliedTaxonsList = () -> + $scope.appliedTaxonsList = -> $scope.activeTaxons.map( (taxon_id) -> Taxons.taxons_by_id[taxon_id].name ).join(" & ") if $scope.activeTaxons? + $scope.appliedPropertiesList = -> + $scope.activeProperties.map( (property_id) -> + Properties.properties_by_id[property_id].name + ).join(" & ") if $scope.activeProperties? + $scope.clearAll = -> $scope.query = "" FilterSelectorsService.clearAll() diff --git a/app/assets/javascripts/darkswarm/filters/properties.js.coffee b/app/assets/javascripts/darkswarm/filters/properties.js.coffee new file mode 100644 index 0000000000..51b02e6634 --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/properties.js.coffee @@ -0,0 +1,16 @@ +Darkswarm.filter 'properties', ()-> + # Filter anything that responds to object.properties + (objects, ids) -> + objects ||= [] + ids ?= [] + if ids.length == 0 + # No properties selected, pass all objects through. + objects + else + objects.filter (obj)-> + properties = obj.properties + # Combine object properties with supplied properties, if they exist. + # properties = properties.concat obj.supplied_properties if obj.supplied_properties + # Match property array. + properties.some (property)-> + property.id in ids diff --git a/app/assets/javascripts/templates/single_line_selectors.html.haml b/app/assets/javascripts/templates/single_line_selectors.html.haml index 5be7e4f7bc..5e9aa57915 100644 --- a/app/assets/javascripts/templates/single_line_selectors.html.haml +++ b/app/assets/javascripts/templates/single_line_selectors.html.haml @@ -7,7 +7,7 @@ %span + {{ overFlowSelectors().length }} more %i.ofn-i_052-point-down - .f-dropdown.content#show-more + .f-dropdown.right.text-left.medium.content#show-more %ul %active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } } %render-svg{path: "{{selector.object.icon}}"} diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass index a714c4a361..554a6ce703 100644 --- a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -27,10 +27,14 @@ .filter-box.filter-box-shopfront, .filter-box.property-box-shopfront background: transparent - single-line-selector + single-line-selectors overflow-x: hidden white-space: nowrap + .f-dropdown + overflow-x: auto + white-space: normal + ul margin: 0 ul, ul li diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 33d566ee1b..df20bb660f 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -10,8 +10,10 @@ %i.ofn-i_009-close %span.filter-label Showing: + %span{ ng: { show: "!query && !appliedPropertiesList() && !appliedTaxonsList()"} } + All %span.applied-properties - Certified Organic + {{ appliedPropertiesList() }} %span.applied-taxons {{ appliedTaxonsList() }} %span.applied-search{ ng: { hide: "!query"} } @@ -28,7 +30,7 @@ %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", - "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons) track by product.id ", "id" => "product-{{ product.id }}"} + "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"} = render partial: "shop/products/summary" %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"} From 4c4490a9b7f66ff1623ff73ac174b8ca0f5a46cb Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 6 Mar 2015 19:01:39 +1100 Subject: [PATCH 20/81] Upgrading foundation-rails and sass --- Gemfile | 2 +- Gemfile.lock | 33 +++++++++++++++++++++------------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index 8998ac8882..07440249c5 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ gem 'angularjs-rails', '1.2.13' gem 'bugsnag' gem 'newrelic_rpm' gem 'haml' -gem 'sass', "~> 3.2" +gem 'sass', "~> 3.3" gem 'sass-rails', '~> 3.2.3', groups: [:default, :assets] gem 'aws-sdk' gem 'db2fog' diff --git a/Gemfile.lock b/Gemfile.lock index 57e0877ad2..540c185899 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -177,7 +177,7 @@ GEM celluloid (0.15.2) timers (~> 1.1.0) chronic (0.10.2) - chunky_png (1.3.0) + chunky_png (1.3.4) climate_control (0.0.3) activesupport (>= 3.0) cliver (0.3.2) @@ -197,12 +197,22 @@ GEM active_link_to (~> 1.0.0) paperclip (>= 2.3.0) rails (>= 3.0.0) - compass (0.12.4) + compass (1.0.3) chunky_png (~> 1.2) - fssm (>= 0.2.7) - sass (~> 3.2.17) - compass-rails (1.0.3) - compass (>= 0.12.2, < 0.14) + compass-core (~> 1.0.2) + compass-import-once (~> 1.0.5) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + sass (>= 3.3.13, < 3.5) + compass-core (1.0.3) + multi_json (~> 1.0) + sass (>= 3.3.0, < 3.5) + compass-import-once (1.0.5) + sass (>= 3.2, < 3.5) + compass-rails (2.0.4) + compass (~> 1.0.0) + sass-rails (<= 5.0.1) + sprockets (< 2.13) crack (0.4.1) safe_yaml (~> 0.9.0) css_parser (1.3.5) @@ -261,10 +271,9 @@ GEM foundation-icons-sass-rails (3.0.0) railties (>= 3.1.1) sass-rails (>= 3.1.1) - foundation-rails (5.2.2.0) + foundation-rails (5.5.0.0) railties (>= 3.1.0) - sass (>= 3.2.0) - fssm (0.2.10) + sass (>= 3.2.0, < 3.4) fuubar (1.3.3) rspec (>= 2.14.0, < 3.1.0) ruby-progressbar (~> 1.4) @@ -332,7 +341,7 @@ GEM railties (>= 3.1) money (5.1.1) i18n (~> 0.6.0) - multi_json (1.10.1) + multi_json (1.11.0) multi_xml (0.5.5) net-scp (1.1.2) net-ssh (>= 2.6.5) @@ -442,7 +451,7 @@ GEM ruby-hmac (0.4.0) ruby-progressbar (1.7.1) safe_yaml (0.9.5) - sass (3.2.19) + sass (3.3.14) sass-rails (3.2.6) railties (~> 3.2.0) sass (>= 3.1.10) @@ -569,7 +578,7 @@ DEPENDENCIES representative_view roadie-rails (~> 1.0.3) rspec-rails - sass (~> 3.2) + sass (~> 3.3) sass-rails (~> 3.2.3) shoulda-matchers simple_form! From 12c6878cbe876470a2a6ea588605d7f706d6d5d8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 6 Mar 2015 19:02:15 +1100 Subject: [PATCH 21/81] Using vanilla foundation --- app/assets/javascripts/darkswarm/all.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 45acfd6523..68bc01b6c1 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -36,4 +36,4 @@ $ -> # Hacky fix for issue - http://foundation.zurb.com/forum/posts/2112-foundation-5100-syntax-error-in-js Foundation.set_namespace "" - #$(document).foundation() + $(document).foundation() From 0a300d18020eaf53a6eb8a07b9ac15c3766429ae Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 6 Mar 2015 19:03:46 +1100 Subject: [PATCH 22/81] Rewriting small cart, so that it doesn't break all of the dropdowns on the page (can't use .row within li elements it seems...) --- .../darkswarm/shopping-cart.css.sass | 34 ++++++++------ app/views/shared/menu/_cart.html.haml | 47 +++++++++---------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass index 72856eba72..0cd0f20fac 100644 --- a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass +++ b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass @@ -20,21 +20,27 @@ right: 22px !important left: auto - ul, li - list-style: none - margin-left: 0 + table + width: 100% + border: none + border-spacing: 0px + margin-bottom: 5px - li - float: none - - .row .columns - padding-left: 0.25rem - padding-right: 0.25rem - - li.total-cart - background-color: #424242 - li.product-cart - border-top: 1px solid #424242 + tr.total-cart + color: #fff + background-color: #424242 + td + color: #fff + tr.product-cart + background-color: #333333 + border-top: 1px solid #424242 + td + padding: 4px 12px + color: #fff + .buttons + .button + height: auto + top: 0px #cart-detail .cart-item-delete diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index 646bd33425..aa7f7940c9 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -10,34 +10,33 @@ %span.joyride-nub.top .joyride-content-wrapper %h5 Your shopping cart - %ul - %li.product-cart{"ng-repeat" => "line_item in Cart.line_items_present()", + %table + %tr.product-cart{"ng-repeat" => "line_item in Cart.line_items_present()", "ng-controller" => "LineItemCtrl", "id" => "cart-variant-{{ line_item.variant.id }}"} - .row - .columns.small-7 - %small - %strong {{ line_item.variant.name_to_display }} - %em {{ line_item.variant.unit_to_display }} - .columns.small-3.text-right - %small - %span.quantity {{ line_item.quantity }} - %i.ofn-i_009-close - %span.price {{ line_item.variant.price_with_fees | localizeCurrency }} + %td + %small + %strong {{ line_item.variant.name_to_display }} + %em {{ line_item.variant.unit_to_display }} + %td.text-right + %small + %span.quantity {{ line_item.quantity }} + %i.ofn-i_009-close + %span.price {{ line_item.variant.price_with_fees | localizeCurrency }} - .columns.small-2 - %small - \= - %strong - .total-price.right {{ line_item.variant.totalPrice() | localizeCurrency }} + %td + %small + \= + %strong + .total-price.right {{ line_item.variant.totalPrice() | localizeCurrency }} - %li.total-cart{"ng-show" => "Cart.line_items_present().length > 0"} - .row - .columns.small-6 - %em Total: - .columns.small-6.text-right - %strong {{ Cart.total() | localizeCurrency }} + %table{"ng-show" => "Cart.line_items_present().length > 0"} + %tr.total-cart + %td + %em Total: + %td.text-right + %strong {{ Cart.total() | localizeCurrency }} - .text-right + .buttons.text-right %a.button.secondary.tiny.add_to_cart{ href: cart_path, type: :submit, "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } {{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }} %a.button.primary.tiny{href: checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"} Checkout now From 274a7a3c7364c287e59504771212bab86e13f176 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 12 Mar 2015 09:56:06 +1100 Subject: [PATCH 23/81] Fixing up the one liner filter styling a bit --- .../templates/active_selector.html.haml | 4 +- .../templates/single_line_selectors.html.haml | 14 +-- .../darkswarm/_shop-filters.css.sass | 112 +++++++++--------- app/views/shop/products/_filters.html.haml | 4 +- 4 files changed, 65 insertions(+), 69 deletions(-) diff --git a/app/assets/javascripts/templates/active_selector.html.haml b/app/assets/javascripts/templates/active_selector.html.haml index 14b12bbd9f..7c9412760e 100644 --- a/app/assets/javascripts/templates/active_selector.html.haml +++ b/app/assets/javascripts/templates/active_selector.html.haml @@ -1,2 +1,2 @@ -%li{"ng-class" => "{active: selector.active}"} - %a{"ng-transclude" => true} +%li{ ng: { class: "{active: selector.active}" } } + %a{ ng: { transclude: true, class: "{active: selector.active}" } } diff --git a/app/assets/javascripts/templates/single_line_selectors.html.haml b/app/assets/javascripts/templates/single_line_selectors.html.haml index 5e9aa57915..e1e96e904a 100644 --- a/app/assets/javascripts/templates/single_line_selectors.html.haml +++ b/app/assets/javascripts/templates/single_line_selectors.html.haml @@ -2,13 +2,13 @@ -# In order for the single-line-selector scope to have access to the available selectors, %filter-selector{objects: "objects()", "active-selectors" => "activeSelectors", "all-selectors" => "allSelectors" } - %li.more{ ng: { show: "overFlowSelectors().length > 0 || fitting", class: "{active: selectedOverFlowSelectors().length > 0}" } } - %a.dropdown{ dropdown: { toggle: "#show-more" } } + %li.more{ ng: { show: "overFlowSelectors().length > 0 || fitting" } } + %a.dropdown{ data: { dropdown: "show-more" }, ng: { class: "{active: selectedOverFlowSelectors().length > 0}" } } %span + {{ overFlowSelectors().length }} more %i.ofn-i_052-point-down - .f-dropdown.right.text-left.medium.content#show-more - %ul - %active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } } - %render-svg{path: "{{selector.object.icon}}"} - %span {{ selector.object.name }} + .f-dropdown.text-right.content#show-more + %ul + %active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } } + %render-svg{path: "{{selector.object.icon}}"} + %span {{ selector.object.name }} diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass index 554a6ce703..238d43816b 100644 --- a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -3,6 +3,51 @@ @import big-input @import animations +@mixin filter-selector($base-clr, $border-clr, $hover-clr) + li + display: inline-block + @include border-radius(0) + padding: 0 + margin: 0 0 0.25rem 0.25rem + &:hover, &:focus + background: transparent + + a + @include border-radius(0.5em) + border: 1px solid $border-clr + padding: 0.625em + font-size: 0.875em + color: $base-clr + font-size: 0.75em + i + font-size: 1.25rem + margin-left: 0.25rem + + render-svg + width: 1.25rem + height: 1.25rem + svg + width: 1.25rem + height: 1.25rem + path + @include csstrans + fill: $base-clr + &:hover, &:focus + color: $hover-clr + render-svg + svg + path + fill: $hover-clr + &.active, &.active:hover, &.active:focus + border: 1px solid $base-clr + background: $base-clr + color: white + render-svg + svg + path + fill: white + + // Alert when search, taxon, filter is triggered .alert-box.search-alert @@ -23,8 +68,7 @@ span.filter-label opacity: 0.75 -// Shopfront taxons -.filter-box.filter-box-shopfront, .filter-box.property-box-shopfront +.filter-box.taxon-selectors, .filter-box.property-selectors background: transparent single-line-selectors @@ -39,61 +83,13 @@ margin: 0 ul, ul li list-style: none - li - display: inline-block - @include border-radius(0) - padding: 0 - margin: 0 0 0.25rem 0.25rem - &:hover, &:focus - background: transparent - li.active - @include box-shadow(none) - a, a:hover, a:focus, a:active, a.active - border: 1px solid $clr-blue - background: $clr-blue - color: white - render-svg - svg - path - fill: white - li a - @include border-radius(0.5em) - border: 1px solid $clr-blue-light - padding: 0.625em - font-size: 0.875em - color: $clr-blue - font-size: 0.75em - i - font-size: 1.25rem - margin-left: 0.25rem - render-svg - width: 1.25rem - height: 1.25rem - svg - width: 1.25rem - height: 1.25rem - path - @include csstrans - fill: $clr-blue - &:hover, &:focus - color: $clr-blue-bright - render-svg - svg - path - fill: $clr-blue-bright -// Shopfront properties -.filter-box.property-box-shopfront - li.active - @include box-shadow(none) - a, a:hover, a:focus, a:active, a.active - border: 1px solid #333 - background: #333 - color: white - li a - border: 1px solid #ccc - color: #888 - &:hover, &:focus - border: 1px solid #999 - color: #666 +.filter-box + // Shopfront taxons + &.taxon-selectors + @include filter-selector($clr-blue, $clr-blue-light, $clr-blue-bright) + + // Shopfront properties + &.property-selectors + @include filter-selector(#666, #ccc, #666) diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 9a347ddee0..9a1eeba931 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,5 +1,5 @@ -.filter-box.filter-box-shopfront.animate-hide.text-right +.filter-box.taxon-selectors.animate-hide.text-right %single-line-selectors{ objects: "Products.products | products:query | taxonsOf", "active-selectors" => "activeTaxons"} -.filter-box.property-box-shopfront.animate-hide.text-right +.filter-box.property-selectors.animate-hide.text-right %single-line-selectors{ objects: "Products.products | products:query | propertiesOf", "active-selectors" => "activeProperties"} From 0258fc24f3debd4437ad14cf6d4d9868f48f8ea6 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 13 Mar 2015 12:58:45 +1100 Subject: [PATCH 24/81] Moving comments to own line. New version of SASS does not like comments on same line as declarations. --- app/assets/stylesheets/darkswarm/active_table.css.sass | 3 ++- app/assets/stylesheets/darkswarm/map.css.sass | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/active_table.css.sass b/app/assets/stylesheets/darkswarm/active_table.css.sass index 27b98850b7..f3a3f0900a 100644 --- a/app/assets/stylesheets/darkswarm/active_table.css.sass +++ b/app/assets/stylesheets/darkswarm/active_table.css.sass @@ -42,7 +42,8 @@ font-size: 0.75rem - .active_table_row // Inherits from active_table + .active_table_row + // Inherits from active_table border: 1px solid transparent @include border-radius(0.5em) diff --git a/app/assets/stylesheets/darkswarm/map.css.sass b/app/assets/stylesheets/darkswarm/map.css.sass index 53e8fb5768..acc280576c 100644 --- a/app/assets/stylesheets/darkswarm/map.css.sass +++ b/app/assets/stylesheets/darkswarm/map.css.sass @@ -10,7 +10,8 @@ height: 100% width: 100% - img // https://github.com/zurb/foundation/issues/112 + img + // https://github.com/zurb/foundation/issues/112 max-width: none height: auto From c8c07ed70088a35aa1b9d1d7740c229764a493ae Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 13 Mar 2015 12:59:26 +1100 Subject: [PATCH 25/81] Adding position fixed to modals - got lost somehow with upgrading foundation zurb --- app/assets/stylesheets/darkswarm/modals.css.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index 578a3ceccb..1d21ae6722 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -32,6 +32,7 @@ dialog, .reveal-modal .reveal-modal-bg background-color: rgba(0,0,0,0.85) + position: fixed dialog .close-reveal-modal, .reveal-modal .close-reveal-modal right: 0.25rem From bce64a1eadeea7fe3888b34e92349b4b1c2e3be3 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 13 Mar 2015 15:34:11 +1100 Subject: [PATCH 26/81] WIP product modals for shopfront --- .../templates/product_modal.html.haml | 29 +++++++++++++++---- .../darkswarm/_shop-modals.css.sass | 6 ++++ 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 app/assets/stylesheets/darkswarm/_shop-modals.css.sass diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index 82cda0a371..f472d9285a 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -1,9 +1,28 @@ .row + + .columns.small-12.large-6.product-header + %h2 {{product.name}} + %span + %em from + %span.avenir {{ enterprise.name }} + + .filter-box.taxon-selectors + %div{ objects: "[product] | taxonsOf"} + + %hr + + .filter-box.property-selectors + %single-line-selectors{ objects: "[product] | propertiesOf"} + + + / %render-svg{path: "{{product.primary_taxon.icon}}"} + + %div{"ng-if" => "product.description"} + %hr + .text-small {{product.description}} + %hr + .columns.small-12.large-6 %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}}"} - {{product.name}} - %p {{product.description}} + %ng-include{src: "'partials/close.html'"} diff --git a/app/assets/stylesheets/darkswarm/_shop-modals.css.sass b/app/assets/stylesheets/darkswarm/_shop-modals.css.sass new file mode 100644 index 0000000000..405c1bc701 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/_shop-modals.css.sass @@ -0,0 +1,6 @@ + +.product-header + h1, h2, h3, h4, h5, h6 + margin: 0 + hr + margin: 0.5em 0 \ No newline at end of file From 88edaceee0041eae91a7d04c68532aade70e9952 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 13 Mar 2015 15:34:38 +1100 Subject: [PATCH 27/81] Adding a new color to brand colors - yellow light --- app/assets/stylesheets/darkswarm/branding.css.sass | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/branding.css.sass b/app/assets/stylesheets/darkswarm/branding.css.sass index d5d319cb2e..71760e23fd 100644 --- a/app/assets/stylesheets/darkswarm/branding.css.sass +++ b/app/assets/stylesheets/darkswarm/branding.css.sass @@ -15,6 +15,8 @@ $clr-blue: #0096ad $clr-blue-light: #85d9e5 $clr-blue-bright: #14b6cc +$clr-yellow-light: #faf6c7 + $disabled-light: #e5e5e5 $disabled-bright: #ccc $disabled-med: #b3b3b3 @@ -24,3 +26,4 @@ $med-grey: #666 $med-drk-grey: #444 $dark-grey: #333 $black: #000 + From e5cc9063e8fb5fd85962a3301e3630b2e1df9d80 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 13 Mar 2015 15:35:45 +1100 Subject: [PATCH 28/81] Changing the medium input styling to make it same height as taxon filters, and no animation to get big as we want the next row space for tags eventually --- app/assets/stylesheets/darkswarm/big-input.sass | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/big-input.sass b/app/assets/stylesheets/darkswarm/big-input.sass index 90bba8cec2..165404fff3 100644 --- a/app/assets/stylesheets/darkswarm/big-input.sass +++ b/app/assets/stylesheets/darkswarm/big-input.sass @@ -49,9 +49,9 @@ @include border-radius(0.5rem) background: rgba(255,255,255,0.1) border: 2px solid $input - font-size: 1rem + font-size: 0.875rem box-shadow: 0 - padding: 0.75rem 1rem 0.35rem + padding: 0.5rem 0.625rem 0.375rem height: auto width: 100% margin-bottom: 0.5rem @@ -69,8 +69,6 @@ background: white background: rgba(255,255,255,0.5) text-shadow: 0 0 10px #ffffff - padding: 1em 1rem 0.75rem - letter-spacing: 0.02rem outline: none @mixin placeholder($placeholder, $placeholderhvr) From e3bc7cf8945bbf6f811eb6a4503aec1f58fb4851 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 13 Mar 2015 15:36:06 +1100 Subject: [PATCH 29/81] Styling for search alert on shopfront page --- app/assets/stylesheets/darkswarm/_shop-filters.css.sass | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass index 238d43816b..13aba876a6 100644 --- a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -51,10 +51,11 @@ // Alert when search, taxon, filter is triggered .alert-box.search-alert - background-color: #faf6c7 - border-color: #faf6c7 - color: #888 + background-color: $clr-yellow-light + border-color: $clr-yellow-light + color: #777 font-size: 0.75rem + padding: 0.5rem 0.75rem span.applied-properties color: #333 From dc1dd2e2439fb0859967482dff5f8dc5078a5924 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 13 Mar 2015 16:06:46 +1100 Subject: [PATCH 30/81] Changing the class .filter-box for shopfront to .filter-shopfront This prevents the issue with inheritance / styling crossover --- .../darkswarm/directives/single_line_selector.coffee | 2 +- app/assets/javascripts/templates/product_modal.html.haml | 8 -------- app/assets/stylesheets/darkswarm/_shop-filters.css.sass | 6 ++++-- .../stylesheets/darkswarm/active_table_search.css.sass | 7 +------ app/views/shop/products/_filters.html.haml | 4 ++-- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee index 53ef2f9e9e..0c4e4b42d7 100644 --- a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee +++ b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee @@ -35,7 +35,7 @@ Darkswarm.directive 'singleLineSelectors', ($timeout, $filter) -> fit = -> used = $(element).find("li.more").outerWidth(true) - available = $(element).parent(".filter-box").innerWidth() + available = $(element).parent(".filter-shopfront").innerWidth() $(element).find("li").not(".more").each (i) -> used += $(this).outerWidth(true) scope.allSelectors[i].fits = used <= available diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index f472d9285a..88e3c58d66 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -6,14 +6,6 @@ %em from %span.avenir {{ enterprise.name }} - .filter-box.taxon-selectors - %div{ objects: "[product] | taxonsOf"} - - %hr - - .filter-box.property-selectors - %single-line-selectors{ objects: "[product] | propertiesOf"} - / %render-svg{path: "{{product.primary_taxon.icon}}"} diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass index 13aba876a6..e68dc9985b 100644 --- a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -11,6 +11,8 @@ margin: 0 0 0.25rem 0.25rem &:hover, &:focus background: transparent + &.active + box-shadow: none a @include border-radius(0.5em) @@ -69,7 +71,7 @@ span.filter-label opacity: 0.75 -.filter-box.taxon-selectors, .filter-box.property-selectors +.filter-shopfront.taxon-selectors, .filter-shopfront.property-selectors background: transparent single-line-selectors @@ -86,7 +88,7 @@ list-style: none -.filter-box +.filter-shopfront // Shopfront taxons &.taxon-selectors @include filter-selector($clr-blue, $clr-blue-light, $clr-blue-bright) diff --git a/app/assets/stylesheets/darkswarm/active_table_search.css.sass b/app/assets/stylesheets/darkswarm/active_table_search.css.sass index d19f116139..49981895d0 100644 --- a/app/assets/stylesheets/darkswarm/active_table_search.css.sass +++ b/app/assets/stylesheets/darkswarm/active_table_search.css.sass @@ -8,7 +8,7 @@ margin-left: 0 margin-right: 0 -.row.filter-box:first-child, .row.filter-box.filter-box-shopfront +.row.filter-box:first-child border: 1px solid $clr-blue-light @include border-radius(0.25em) margin-top: 2px @@ -19,11 +19,6 @@ background: transparent margin-top: 1em -.row.filter-box.filter-box-shopfront - margin-top: 0 - background: transparent - border: 0 none - products .filter-box background: #f7f7f7 diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 9a1eeba931..7e9fc6555d 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,5 +1,5 @@ -.filter-box.taxon-selectors.animate-hide.text-right +.filter-shopfront.taxon-selectors.animate-hide.text-right %single-line-selectors{ objects: "Products.products | products:query | taxonsOf", "active-selectors" => "activeTaxons"} -.filter-box.property-selectors.animate-hide.text-right +.filter-shopfront.property-selectors.animate-hide.text-right %single-line-selectors{ objects: "Products.products | products:query | propertiesOf", "active-selectors" => "activeProperties"} From 7b1901253cf34c7c34e3953342ed8ce686e237d4 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 13 Mar 2015 17:32:33 +1100 Subject: [PATCH 31/81] More work on filters for shopfront rewriting as compared to the rest of filter button styles --- .../darkswarm/_shop-filters.css.sass | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass index e68dc9985b..be1da5a877 100644 --- a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -15,22 +15,23 @@ box-shadow: none a + display: block + padding-top: 0.5rem @include border-radius(0.5em) border: 1px solid $border-clr - padding: 0.625em + padding: 0.5em 0.625em font-size: 0.875em color: $base-clr font-size: 0.75em i - font-size: 1.25rem - margin-left: 0.25rem + padding-left: 0.25rem render-svg - width: 1.25rem - height: 1.25rem - svg - width: 1.25rem - height: 1.25rem + &, & svg + width: 1rem + height: 1rem + float: left + padding-right: 0.25rem path @include csstrans fill: $base-clr @@ -89,6 +90,7 @@ .filter-shopfront + // Shopfront taxons &.taxon-selectors @include filter-selector($clr-blue, $clr-blue-light, $clr-blue-bright) @@ -96,3 +98,5 @@ // Shopfront properties &.property-selectors @include filter-selector(#666, #ccc, #666) + + From 2c1ef4c8c1a446e2e4e3a12cea9ae204188cba6c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 15 Mar 2015 12:27:40 +1100 Subject: [PATCH 32/81] Refactoring single line selectors to remove flicker --- .../directives/single_line_selector.coffee | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee index 0c4e4b42d7..c284adcd5c 100644 --- a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee +++ b/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee @@ -33,22 +33,39 @@ Darkswarm.directive 'singleLineSelectors', ($timeout, $filter) -> func.apply subject, Array::slice.call(args) , timeout) + loadWidths = -> + $(element).find("li").not(".more").each (i) -> + scope.allSelectors[i].width = $(this).outerWidth(true) + return null # So we don't exit the loop weirdly + + fit = -> used = $(element).find("li.more").outerWidth(true) - available = $(element).parent(".filter-shopfront").innerWidth() - $(element).find("li").not(".more").each (i) -> - used += $(this).outerWidth(true) - scope.allSelectors[i].fits = used <= available - return null # So we don't exit the loop on false + used += selector.width for selector in scope.allSelectors when selector.fits + available = $(element).parent(".filter-shopfront").innerWidth() - used + if available > 0 + for selector in scope.allSelectors when !selector.fits + available -= selector.width + selector.fits = true if available > 0 + else + for i in [scope.allSelectors.length-1..0] + selector = scope.allSelectors[i] + if !selector.fits + continue + else + if available < 0 + selector.fits = false + available += selector.width scope.fitting = false scope.$watchCollection "allSelectors", -> if scope.allSelectors? scope.fitting = true selector.fits = true for selector in scope.allSelectors - $timeout fit, 0, true + loadWidths() + fit() $(window).resize debouncer (e) -> scope.fitting = true - scope.$apply -> selector.fits = true for selector in scope.allSelectors - $timeout fit, 0, true + if scope.allSelectors? + $timeout fit, 0, true From 29c9f70a1c7b6008edb056c0bcd1d62d1e155883 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 15 Mar 2015 12:29:05 +1100 Subject: [PATCH 33/81] Rename single line selectors --- .../{single_line_selector.coffee => single_line_selectors.coffee} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/darkswarm/directives/{single_line_selector.coffee => single_line_selectors.coffee} (100%) diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selector.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee similarity index 100% rename from app/assets/javascripts/darkswarm/directives/single_line_selector.coffee rename to app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee From 2b32252affea4d5e7a992cfe90afb29366e3f5e7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 15 Mar 2015 13:25:04 +1100 Subject: [PATCH 34/81] Filtering between selector lists --- .../darkswarm/directives/filter_selector.js.coffee | 3 +++ .../darkswarm/directives/single_line_selectors.coffee | 4 ++-- app/views/shop/products/_filters.html.haml | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee index c904a16b8d..5232906016 100644 --- a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee @@ -25,6 +25,9 @@ Darkswarm.directive "filterSelector", (FilterSelectorsService)-> scope.$on 'loadFilterSelectors', -> scope.allSelectors = scope.selectors() + scope.$watchCollection "selectors()", (newValue, oldValue) -> + scope.allSelectors = scope.selectors() + # Build a list of selectors scope.selectors = -> # Generate a selector for each object. diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee index c284adcd5c..ed756d8fde 100644 --- a/app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee +++ b/app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee @@ -62,8 +62,8 @@ Darkswarm.directive 'singleLineSelectors', ($timeout, $filter) -> if scope.allSelectors? scope.fitting = true selector.fits = true for selector in scope.allSelectors - loadWidths() - fit() + $timeout(loadWidths, 0, true).then -> + $timeout fit, 0, true $(window).resize debouncer (e) -> scope.fitting = true diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 7e9fc6555d..90f6ff3121 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,5 +1,5 @@ .filter-shopfront.taxon-selectors.animate-hide.text-right - %single-line-selectors{ objects: "Products.products | products:query | taxonsOf", "active-selectors" => "activeTaxons"} + %single-line-selectors{ objects: "Products.products | products:query | properties: activeProperties | taxonsOf", "active-selectors" => "activeTaxons"} .filter-shopfront.property-selectors.animate-hide.text-right - %single-line-selectors{ objects: "Products.products | products:query | propertiesOf", "active-selectors" => "activeProperties"} + %single-line-selectors{ objects: "Products.products | products:query | taxons:activeTaxons | propertiesOf", "active-selectors" => "activeProperties"} From bfe7f49033747eca0aeed9c9fb1bc3427d6802cb Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 18 Mar 2015 12:01:53 +1100 Subject: [PATCH 35/81] Show and hide search box --- app/views/shop/products/_form.html.haml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index df20bb660f..a5efba3ced 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -2,16 +2,14 @@ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} // TODO: Needs an ng-show to slide content down - .row.animate-hide + .row.animate-hide.animate-show{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" } .small-12.columns - .alert-box.search-alert.ng-scope{"ng-show" => "visible", "ofn-inline-alert" => ""} + .alert-box.search-alert.ng-scope %a.right{"ng-click" => "clearAll()"} Clear all %i.ofn-i_009-close %span.filter-label Showing: - %span{ ng: { show: "!query && !appliedPropertiesList() && !appliedTaxonsList()"} } - All %span.applied-properties {{ appliedPropertiesList() }} %span.applied-taxons From 4e54a3c48e74faa834121ea4b99ef54e4170dc15 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 19 Mar 2015 14:50:50 +1100 Subject: [PATCH 36/81] Updating no image all grey, large version larger --- app/assets/images/noimage/large.png | Bin 63558 -> 15243 bytes app/assets/images/noimage/mini.png | Bin 4888 -> 1541 bytes app/assets/images/noimage/product.png | Bin 25146 -> 3971 bytes app/assets/images/noimage/small.png | Bin 3841 -> 3971 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/images/noimage/large.png b/app/assets/images/noimage/large.png index c9b2af198c8437463f419ee5d40bcc3c3a3c4133..29dcff5ea9ece92eac651ec6ed02263812126624 100644 GIT binary patch literal 15243 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 diff --git a/app/assets/images/noimage/mini.png b/app/assets/images/noimage/mini.png index dac04c5863c7e2a7b589a30948d43d4d0fb5bb0f..5094c92a18842f1fc8e214b8142b8897b0248280 100644 GIT binary patch literal 1541 zcmZvcc{J2(7{`B%#ySk?W{fqa5OJ?biAFKDFocj2S?WqDTj|mXMMxNX%vG1M4p=l(!rJI7BT>t@XjMzrTOrhYbx4?J@|%!^0r|uYua#JP4zsqk9@K-0Sdj za!(8bjYium?ZV*T;Ln2pRL951_cA~3`0r>iIXOA`^G)_-3jbrs~bwKb4O zMn=HW#KZ*XLEZ%>lQ}jv#%8nWbUNt4CR8f*jcjZt_%8OomN+ZmfOrsBgpmNRbed0L z07h=5y=Zey^bEFIe5&*aijIn2}jatYFEpfvD9BYV}QpQ4! zCkZ~#*Q_ERsqGyd0e}8i7l$yLlD;X-qExAJuAL|I&W(-9Tb1l54?Q%!7J}7Id8(&c zlS*9h>ou4`=MpUn{Y&_kN`>RetEzgNm+oSGD9}tRmdtzGjyVgWD#$kx)&U-nwr1 z9M;;9WEKF-S;?UARo(lwzxP=r66N``&9C(rL??G7S=m3ZpUHQ>8&?6y>>Ohz4BR1` z)5`Us)n4h|>VNv7Rc(#DoBajT6*yNJD%rD<->7P)9_shG03|Y6Yf>I1oYNrc*tFmD zgt-QtE>Sww4h$`SNOsxQ`6-p=SD2JXGRVGfc}PaIvm50!puJ=Zn&Op|U zm+7zHt+E2M=b{@>N6fyKOmQ@4C5WmZ%2G$^iw~TvK2y!f;NSb#yOdHn**K^Di zk;WJrE8F-6`U%$NYSiR~XL%xXA0u2t93(1=?Wagm4+Ou93ungu;sfhAEB0ljF7tqY z_P*qr3NmW(P$t>}cAWTW$#u-cC`Te{Lt8E|Z88N`eFs6k}RY zrMi^zTQl^4Pq@D z*Ivdh0)~3{431(o4(m&P-(H%3sMBJS8HDS&WUrwgQEHSGa6BqFI=sdGj)$E`c)1b> zK6g}+1PT3!2$hE?tDQtf->M?33MTyJ%1T4^j&m68ye(~2_#m1x{XIEWb;=En4F342 zXgQ@T%`{{Ro}k@aud86=ZmFDi<~Eev806*Al}#IIVAxR{y!eaT(|M|{TFqibyw(rm zmC`6v`Avbx61`Vv8PI^`&z*C6YvkwbCFykzO-|ya1r>TH_-_Hm`Z#Qnu2ab0c>_U& literal 4888 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 KMNUMnLSTXfcWr|J diff --git a/app/assets/images/noimage/product.png b/app/assets/images/noimage/product.png index 7bb97178b90d53b3df7f5bde20fa2e976365fdb5..ca06da639bb1d73230f50b6cadf19490d4fd23cf 100644 GIT binary patch literal 3971 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 Date: Thu, 19 Mar 2015 14:51:23 +1100 Subject: [PATCH 37/81] Animation WIP --- .../stylesheets/darkswarm/animations.sass | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/animations.sass b/app/assets/stylesheets/darkswarm/animations.sass index 544e2823ce..561c129069 100644 --- a/app/assets/stylesheets/darkswarm/animations.sass +++ b/app/assets/stylesheets/darkswarm/animations.sass @@ -2,6 +2,26 @@ // ANIMATION FUNCTIONS + +@-webkit-keyframes heightSlideIn + 0% + opacity: 0 + height: 0 + 100% + opacity: 100 + height: 30px + +@keyframes heightSlideIn + 0% + opacity: 0 + height: 0 + 100% + opacity: 100 + height: 30px + + + +// @-webkit-keyframes slideInDown 0% opacity: 0 @@ -22,6 +42,8 @@ -ms-transform: translateY(0) transform: translateY(0) + + @-webkit-keyframes slideOutUp 0% -webkit-transform: translateY(0) @@ -160,6 +182,35 @@ product.animate-repeat -webkit-animation-fill-mode: both animation-fill-mode: both +// + +.animate-slide + -webkit-animation-name: heightSlideIn + animation-name: heightSlideIn + -webkit-animation-fill-mode: both + animation-fill-mode: both + -webkit-transition: all 500ms ease-in-out + -moz-transition: all 500ms ease-in-out + -ms-transition: all 500ms ease-in-out + -o-transition: all 500ms ease-in-out + transition: all 500ms ease-in-out + +.animate-slide.ng-hide + -webkit-animation-name: slideOutUp + animation-name: slideOutUp + -webkit-animation-duration: 0.15s + animation-duration: 0.15s + -webkit-animation-fill-mode: both + animation-fill-mode: both + +.row.animate-slide ~ .row + -webkit-animation-name: fadeIn + animation-name: fadeIn + -webkit-animation-duration: 0.5s + animation-duration: 0.5s + -webkit-animation-fill-mode: both + animation-fill-mode: both + @mixin csstrans -webkit-transition: all 300ms ease From 3c61bf9cc4058632b38fb9bdf5ec0edd6fd728fa Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 19 Mar 2015 14:51:41 +1100 Subject: [PATCH 38/81] Styling for no image available on product modal overlay --- app/assets/stylesheets/darkswarm/images.css.sass | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/images.css.sass b/app/assets/stylesheets/darkswarm/images.css.sass index 798e656481..b89cbb21fc 100644 --- a/app/assets/stylesheets/darkswarm/images.css.sass +++ b/app/assets/stylesheets/darkswarm/images.css.sass @@ -7,6 +7,11 @@ margin-bottom: 10px outline: 1px solid #ccc @include box-shadow(0 1px 2px 1px rgba(0,0,0,0.15)) + // placeholder for when no product images + &.placeholder + opacity: 0.35 + @media all and (max-width: 1024px) + display: none .hero-img outline: 1px solid $disabled-bright From cdd41ad6517d165b7179b11a0f37e85924d97af8 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 19 Mar 2015 14:52:00 +1100 Subject: [PATCH 39/81] change animation class --- app/views/shop/products/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index a5efba3ced..1147e6c1dd 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -2,7 +2,7 @@ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} // TODO: Needs an ng-show to slide content down - .row.animate-hide.animate-show{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" } + .row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" } .small-12.columns .alert-box.search-alert.ng-scope %a.right{"ng-click" => "clearAll()"} From c76aa1d1c239ac7202409ff03368dccc13103913 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 19 Mar 2015 14:52:09 +1100 Subject: [PATCH 40/81] WIP on product modal --- app/assets/javascripts/templates/product_modal.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index 88e3c58d66..244c312666 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -1,20 +1,20 @@ .row .columns.small-12.large-6.product-header - %h2 {{product.name}} + %h3 {{product.name}} %span %em from %span.avenir {{ enterprise.name }} - - + / %render-svg{path: "{{product.primary_taxon.icon}}"} %div{"ng-if" => "product.description"} %hr - .text-small {{product.description}} + %p.text-small {{product.description}} %hr .columns.small-12.large-6 %img.product-img{"ng-src" => "{{product.largeImage}}", "ng-if" => "product.largeImage"} + %img.product-img.placeholder{"ng-src" => "/assets/noimage/large.png", "ng-if" => "!product.largeImage"} %ng-include{src: "'partials/close.html'"} From 1f970529549c1bdf508a896cb98e59a06d02b95d Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 19 Mar 2015 15:05:14 +1100 Subject: [PATCH 41/81] Add a min height to modals so they are not quite so stupidly small when not enough content suppied by users --- app/assets/stylesheets/darkswarm/modals.css.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index 1d21ae6722..560153c5b1 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -8,6 +8,7 @@ dialog, .reveal-modal border-bottom: 30px solid white overflow-y: scroll overflow-x: hidden + min-height: 260px // Not working yet - want a nice gradient shadow when there is overflow - needs JS too // &:after // @include elipse-shadow(0 0 40px rgba(0, 0, 0, 0.8)) From b99e94cecf5bbe2389067eaa39b4f344ea06b975 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 19 Mar 2015 16:02:52 +1100 Subject: [PATCH 42/81] WIP on producer and product modals --- .../partials/enterprise_details.html.haml | 16 +++++++++++++ .../templates/product_modal.html.haml | 17 ++++++++++++- .../darkswarm/_shop-filters.css.sass | 24 +++++++++++++++++-- app/views/shop/products/_filters.html.haml | 4 ++-- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/templates/partials/enterprise_details.html.haml b/app/assets/javascripts/templates/partials/enterprise_details.html.haml index 90741a5a8c..5cee30651b 100644 --- a/app/assets/javascripts/templates/partials/enterprise_details.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_details.html.haml @@ -1,5 +1,21 @@ .row{bindonce: true} .small-12.large-8.columns + / TODO: Rob - add in taxons and properties and property pop-overs + .pad-top{"ng-if" => "1 > 0"} + %span.filter-shopfront.taxon-selectors + %ul.inline-block + %li + %a.button.tiny.disabled Grains + %li + %a.button.tiny.disabled Dairy + + %span.filter-shopfront.property-selectors.pad-top + %ul.inline-block + %li + %a.button.tiny Organic certified + / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ + + %div{"ng-if" => "enterprise.long_description.length > 0 || enterprise.logo"} %p.modal-header About .about-container diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index 244c312666..b812424cc7 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -5,8 +5,23 @@ %span %em from %span.avenir {{ enterprise.name }} - + + / TODO: Rob - add in taxons and properties and property pop-overs / %render-svg{path: "{{product.primary_taxon.icon}}"} + .pad-top + %span.filter-shopfront.taxon-selectors + %ul.inline-block + %li + %a.button.tiny.disabled Grains + %li + %a.button.tiny.disabled Dairy + + %span.filter-shopfront.property-selectors.pad-top + %ul.inline-block + %li + %a.button.tiny Organic certified + / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ + %div{"ng-if" => "product.description"} %hr diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass index be1da5a877..0d59f260f4 100644 --- a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -4,6 +4,9 @@ @import animations @mixin filter-selector($base-clr, $border-clr, $hover-clr) + ul.inline-block + display: inline-block + li display: inline-block @include border-radius(0) @@ -14,7 +17,7 @@ &.active box-shadow: none - a + a, a.button display: block padding-top: 0.5rem @include border-radius(0.5em) @@ -23,6 +26,8 @@ font-size: 0.875em color: $base-clr font-size: 0.75em + background: white + margin: 0 i padding-left: 0.25rem @@ -35,12 +40,27 @@ path @include csstrans fill: $base-clr + &:hover, &:focus + border-color: $hover-clr color: $hover-clr render-svg svg path fill: $hover-clr + + &.disabled + opacity: 0.6 + + &:hover, &:focus + border-color: $border-clr + color: $base-clr + render-svg + svg + path + fill: $base-clr + + &.active, &.active:hover, &.active:focus border: 1px solid $base-clr background: $base-clr @@ -97,6 +117,6 @@ // Shopfront properties &.property-selectors - @include filter-selector(#666, #ccc, #666) + @include filter-selector(#666, #ccc, #777) diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index 90f6ff3121..786cd48662 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,5 +1,5 @@ -.filter-shopfront.taxon-selectors.animate-hide.text-right +.filter-shopfront.taxon-selectors.text-right %single-line-selectors{ objects: "Products.products | products:query | properties: activeProperties | taxonsOf", "active-selectors" => "activeTaxons"} -.filter-shopfront.property-selectors.animate-hide.text-right +.filter-shopfront.property-selectors.text-right %single-line-selectors{ objects: "Products.products | products:query | taxons:activeTaxons | propertiesOf", "active-selectors" => "activeProperties"} From 938eff84820c302c3e35a830f89f40a610d053b0 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 19 Mar 2015 16:13:47 +1100 Subject: [PATCH 43/81] More layout tweaks to enterprise modal template --- .../partials/enterprise_details.html.haml | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/templates/partials/enterprise_details.html.haml b/app/assets/javascripts/templates/partials/enterprise_details.html.haml index 5cee30651b..5593976838 100644 --- a/app/assets/javascripts/templates/partials/enterprise_details.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_details.html.haml @@ -1,24 +1,26 @@ .row{bindonce: true} .small-12.large-8.columns - / TODO: Rob - add in taxons and properties and property pop-overs - .pad-top{"ng-if" => "1 > 0"} - %span.filter-shopfront.taxon-selectors - %ul.inline-block - %li - %a.button.tiny.disabled Grains - %li - %a.button.tiny.disabled Dairy - - %span.filter-shopfront.property-selectors.pad-top - %ul.inline-block - %li - %a.button.tiny Organic certified - / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ - - - %div{"ng-if" => "enterprise.long_description.length > 0 || enterprise.logo"} + / TODO: Rob add logic for taxons and properties too: + / %div{"ng-if" => "enterprise.long_description.length > 0 || enterprise.logo"} + %div %p.modal-header About - .about-container + / TODO: Rob - add in taxons and properties and property pop-overs + + %div + %span.filter-shopfront.taxon-selectors + %ul.inline-block + %li + %a.button.tiny.disabled Grains + %li + %a.button.tiny.disabled Dairy + + %span.filter-shopfront.property-selectors.pad-top + %ul.inline-block + %li + %a.button.tiny Organic certified + / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ + + .about-container.pad-top %img.enterprise-logo{"bo-src" => "enterprise.logo", "bo-if" => "enterprise.logo"} %p.text-small{"ng-bind-html" => "enterprise.long_description"} .small-12.large-4.columns From 0b1857771e9cc4ca4fcceb418d8f97a2b72ddb97 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 20 Mar 2015 13:09:45 +1100 Subject: [PATCH 44/81] Finally got this slide animation working. stupid angular documentation! ref: http://www.yearofmoo.com/2013/08/remastered-animation-in-angularjs-1-2.html --- .../stylesheets/darkswarm/animations.sass | 62 +++++++------------ 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/animations.sass b/app/assets/stylesheets/darkswarm/animations.sass index 561c129069..479caba986 100644 --- a/app/assets/stylesheets/darkswarm/animations.sass +++ b/app/assets/stylesheets/darkswarm/animations.sass @@ -3,24 +3,6 @@ // ANIMATION FUNCTIONS -@-webkit-keyframes heightSlideIn - 0% - opacity: 0 - height: 0 - 100% - opacity: 100 - height: 30px - -@keyframes heightSlideIn - 0% - opacity: 0 - height: 0 - 100% - opacity: 100 - height: 30px - - - // @-webkit-keyframes slideInDown 0% @@ -185,31 +167,29 @@ product.animate-repeat // .animate-slide - -webkit-animation-name: heightSlideIn - animation-name: heightSlideIn - -webkit-animation-fill-mode: both - animation-fill-mode: both - -webkit-transition: all 500ms ease-in-out - -moz-transition: all 500ms ease-in-out - -ms-transition: all 500ms ease-in-out - -o-transition: all 500ms ease-in-out - transition: all 500ms ease-in-out + max-height: 500px + opacity: 1 !important + -webkit-transition: all 300ms ease-in-out + -moz-transition: all 300ms ease-in-out + -o-transition: all 300ms ease-in-out + transition: all 300ms ease-in-out -.animate-slide.ng-hide - -webkit-animation-name: slideOutUp - animation-name: slideOutUp - -webkit-animation-duration: 0.15s - animation-duration: 0.15s - -webkit-animation-fill-mode: both - animation-fill-mode: both + &.ng-hide + overflow: hidden + max-height: 0 + opacity: 0 !important + + // &.ng-hide-add-active, &.ng-hide-remove-active -.row.animate-slide ~ .row - -webkit-animation-name: fadeIn - animation-name: fadeIn - -webkit-animation-duration: 0.5s - animation-duration: 0.5s - -webkit-animation-fill-mode: both - animation-fill-mode: both + &.ng-hide-add, &.ng-hide-remove + /* IMPORTANT: this needs to be here to make it visible during the animation + since the .ng-hide class is already on the element rendering + it as hidden. */ + display: block !important + + + + @mixin csstrans From 7090bb518bbc0f12e36d62266cc74ad46e2ee84a Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 20 Mar 2015 14:24:59 +1100 Subject: [PATCH 45/81] Tweaking logic for alert bar to make messages more human readable for all use cases. --- .../stylesheets/darkswarm/animations.sass | 20 ++++++++++++++++++- app/views/shop/products/_form.html.haml | 6 ++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/animations.sass b/app/assets/stylesheets/darkswarm/animations.sass index 479caba986..62e87e7224 100644 --- a/app/assets/stylesheets/darkswarm/animations.sass +++ b/app/assets/stylesheets/darkswarm/animations.sass @@ -187,7 +187,25 @@ product.animate-repeat it as hidden. */ display: block !important - + +.animate-show + opacity: 1 !important + -webkit-transition: all 300ms ease-in-out + -moz-transition: all 300ms ease-in-out + -o-transition: all 300ms ease-in-out + transition: all 300ms ease-in-out + + &.ng-hide + opacity: 0 !important + + // &.ng-hide-add-active, &.ng-hide-remove-active + + &.ng-hide-add, &.ng-hide-remove + /* IMPORTANT: this needs to be here to make it visible during the animation + since the .ng-hide class is already on the element rendering + it as hidden. */ + display: block !important + diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 1147e6c1dd..0df4f022aa 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -14,8 +14,10 @@ {{ appliedPropertiesList() }} %span.applied-taxons {{ appliedTaxonsList() }} - %span.applied-search{ ng: { hide: "!query"} } - with {{ query }} + %span{ ng: { hide: "!query"} } + %span{ "ng-show" => "appliedPropertiesList() || appliedTaxonsList()" } + with + %span.applied-search "{{ query }}" .row .small-12.medium-6.large-5.columns %input#search.text{"ng-model" => "query", From 0c155e6e3ac63a82b3d4e0e1b1fa6666085a4a36 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 15 Apr 2015 15:32:17 +1000 Subject: [PATCH 46/81] Display message when email has not been confirmed for new enterprise --- app/models/enterprise.rb | 5 +++++ app/views/admin/enterprises/form/_contact.html.haml | 11 ++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 78a40b7c60..72f7ba85cd 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -320,6 +320,11 @@ class Enterprise < ActiveRecord::Base end end + # Based on a devise method, but without adding errors + def pending_any_confirmation? + !confirmed? || pending_reconfirmation? + end + protected def devise_mailer diff --git a/app/views/admin/enterprises/form/_contact.html.haml b/app/views/admin/enterprises/form/_contact.html.haml index c16f93f4b0..df28b6a921 100644 --- a/app/views/admin/enterprises/form/_contact.html.haml +++ b/app/views/admin/enterprises/form/_contact.html.haml @@ -1,9 +1,10 @@ --if @enterprise.unconfirmed_email +-if @enterprise.pending_any_confirmation? .alert-box - Email change is pending. + - email = @enterprise.confirmed? ? @enterprise.unconfirmed_email : @enterprise.email + Email confirmation is pending. We've sent a confirmation email to - %strong= "#{@enterprise.unconfirmed_email}." - = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.unconfirmed_email } ), method: :post) + %strong= "#{email}." + = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: email } ), method: :post) %a.close{ href: "#" } × .row .alpha.three.columns @@ -30,4 +31,4 @@ .alpha.three.columns = f.label :website .omega.eight.columns - = f.text_field :website, { placeholder: "eg. www.truffles.com"} \ No newline at end of file + = f.text_field :website, { placeholder: "eg. www.truffles.com"} From 5940ff2b2cf10c1bdd14eabe8723b438dd508ff4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 16 Apr 2015 16:56:27 +1000 Subject: [PATCH 47/81] Don't override devise's after_sign_in_path_for, use specific before filters for user sessions and registrations controllers instead --- app/controllers/application_controller.rb | 7 ++-- .../user_sessions_controller_decorator.rb | 2 ++ .../user_registrations_controller.rb | 1 + .../spree/user_sessions_controller_spec.rb | 31 +++++++++++++++++ .../user_registrations_controller_spec.rb | 34 ++++++++++++++----- 5 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 spec/controllers/spree/user_sessions_controller_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e96a0f316e..53763ad274 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -8,15 +8,12 @@ class ApplicationController < ActionController::Base super(options, response_status) end - def after_sign_in_path_for(resource) + def set_checkout_redirect if request.referer and referer_path = URI(request.referer).path - [main_app.checkout_path].include?(referer_path) ? referer_path : root_path - else - root_path + session["spree_user_return_to"] = [main_app.checkout_path].include?(referer_path) ? referer_path : root_path end end - private def require_distributor_chosen diff --git a/app/controllers/spree/user_sessions_controller_decorator.rb b/app/controllers/spree/user_sessions_controller_decorator.rb index c0d5d72381..72dc8b6d14 100644 --- a/app/controllers/spree/user_sessions_controller_decorator.rb +++ b/app/controllers/spree/user_sessions_controller_decorator.rb @@ -1,4 +1,6 @@ Spree::UserSessionsController.class_eval do + before_filter :set_checkout_redirect, only: :create + def create authenticate_spree_user! diff --git a/app/controllers/user_registrations_controller.rb b/app/controllers/user_registrations_controller.rb index a6a0074553..5728de6672 100644 --- a/app/controllers/user_registrations_controller.rb +++ b/app/controllers/user_registrations_controller.rb @@ -1,4 +1,5 @@ class UserRegistrationsController < Spree::UserRegistrationsController + before_filter :set_checkout_redirect, only: :create # POST /resource/sign_up def create diff --git a/spec/controllers/spree/user_sessions_controller_spec.rb b/spec/controllers/spree/user_sessions_controller_spec.rb new file mode 100644 index 0000000000..b4235db82b --- /dev/null +++ b/spec/controllers/spree/user_sessions_controller_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Spree::UserSessionsController do + include AuthenticationWorkflow + + let(:user) { create_enterprise_user } + + before do + @request.env["devise.mapping"] = Devise.mappings[:spree_user] + end + + describe "create" do + context "succeed" do + context "when referer is not '/checkout'" do + it "redirects to root" do + spree_post :create, spree_user: {email: user.email, password: user.password }, :use_route => :spree + response.should redirect_to root_path + end + end + + context "when referer is '/checkout'" do + before { @request.env['HTTP_REFERER'] = 'http://test.com/checkout' } + + it "redirects to checkout" do + spree_post :create, spree_user: { email: user.email, password: user.password }, :use_route => :spree + response.should redirect_to checkout_path + end + end + end + end +end diff --git a/spec/controllers/user_registrations_controller_spec.rb b/spec/controllers/user_registrations_controller_spec.rb index ed47f0c059..baa4cb73c7 100644 --- a/spec/controllers/user_registrations_controller_spec.rb +++ b/spec/controllers/user_registrations_controller_spec.rb @@ -6,7 +6,7 @@ describe UserRegistrationsController do before do @request.env["devise.mapping"] = Devise.mappings[:spree_user] end - + describe "via ajax" do render_views it "returns errors when registration fails" do @@ -25,15 +25,31 @@ describe UserRegistrationsController do end end - it "renders new when registration fails" do - spree_post :create, spree_user: {} - response.status.should == 200 - response.should render_template "spree/user_registrations/new" + context "when registration fails" do + it "renders new" do + spree_post :create, spree_user: {} + response.status.should == 200 + response.should render_template "spree/user_registrations/new" + end end - it "redirects when registration succeeds" do - spree_post :create, spree_user: {email: "test@test.com", password: "testy123", password_confirmation: "testy123"}, :use_route => :spree - response.should be_redirect - assigns[:user].email.should == "test@test.com" + context "when registration succeeds" do + context "when referer is not '/checkout'" do + it "redirects to root" do + spree_post :create, spree_user: {email: "test@test.com", password: "testy123", password_confirmation: "testy123"}, :use_route => :spree + response.should redirect_to root_path + assigns[:user].email.should == "test@test.com" + end + end + + context "when referer is '/checkout'" do + before { @request.env['HTTP_REFERER'] = 'http://test.com/checkout' } + + it "redirects to checkout" do + spree_post :create, spree_user: {email: "test@test.com", password: "testy123", password_confirmation: "testy123"}, :use_route => :spree + response.should redirect_to checkout_path + assigns[:user].email.should == "test@test.com" + end + end end end From afe77925bad4e542a7e3fcc17c8ee962eeb72bf5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 17 Apr 2015 10:21:25 +1000 Subject: [PATCH 48/81] Allow request to specify a return value when calling user_passwords#edit --- app/controllers/user_passwords_controller.rb | 8 ++++++ .../user_passwords_controller_spec.rb | 25 +++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/controllers/user_passwords_controller.rb b/app/controllers/user_passwords_controller.rb index ed3be07f35..1870cc8859 100644 --- a/app/controllers/user_passwords_controller.rb +++ b/app/controllers/user_passwords_controller.rb @@ -1,6 +1,8 @@ class UserPasswordsController < Spree::UserPasswordsController layout 'darkswarm' + before_filter :set_admin_redirect, only: :edit + def create self.resource = resource_class.send_reset_password_instructions(params[resource_name]) @@ -18,4 +20,10 @@ class UserPasswordsController < Spree::UserPasswordsController end end end + + private + + def set_admin_redirect + session["spree_user_return_to"] = params[:return_to] if params[:return_to] + end end diff --git a/spec/controllers/user_passwords_controller_spec.rb b/spec/controllers/user_passwords_controller_spec.rb index 25c285e09b..686d46656e 100644 --- a/spec/controllers/user_passwords_controller_spec.rb +++ b/spec/controllers/user_passwords_controller_spec.rb @@ -9,15 +9,26 @@ describe UserPasswordsController do ActionMailer::Base.default_url_options[:host] = "test.host" end - it "returns errors" do - spree_post :create, spree_user: {} - response.should be_success - response.should render_template "spree/user_passwords/new" + describe "create" do + it "returns errors" do + spree_post :create, spree_user: {} + response.should be_success + response.should render_template "spree/user_passwords/new" + end + + it "redirects to login when data is valid" do + spree_post :create, spree_user: { email: user.email} + response.should be_redirect + end end - it "redirects to login when data is valid" do - spree_post :create, spree_user: { email: user.email} - response.should be_redirect + describe "edit" do + context "when given a redirect" do + it "stores the redirect path in 'spree_user_return_to'" do + spree_post :edit, reset_password_token: "token", return_to: "/return_path" + expect(session["spree_user_return_to"]).to eq "/return_path" + end + end end it "renders Darkswarm" do From 456a6f94f5dba03d5129b85fa674cd203480f79c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 17 Apr 2015 10:24:37 +1000 Subject: [PATCH 49/81] On enterprise confirmation: create a new user based on enterprise contact email if one does not already exist, and add it as a manager --- .../enterprise_confirmations_controller.rb | 22 +++++++- ...nterprise_confirmations_controller_spec.rb | 53 +++++++++++++++---- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/app/controllers/enterprise_confirmations_controller.rb b/app/controllers/enterprise_confirmations_controller.rb index b9868c6f98..d275260599 100644 --- a/app/controllers/enterprise_confirmations_controller.rb +++ b/app/controllers/enterprise_confirmations_controller.rb @@ -32,6 +32,24 @@ class EnterpriseConfirmationsController < DeviseController set_flash_message(:error, :not_confirmed) if is_navigational_format? end - respond_with_navigational(resource){ redirect_to spree.admin_path } + respond_with_navigational(resource){ redirect_to redirect_path(resource) } end -end \ No newline at end of file + + private + + def new_user_reset_path(resource) + password = Devise.friendly_token.first(8) + user = Spree::User.create(email: resource.email, password: password, password_confirmation: password) + user.send_reset_password_instructions + resource.users << user + spree.edit_spree_user_password_path(user, :reset_password_token => user.reset_password_token, return_to: spree.admin_path) + end + + def redirect_path(resource) + if resource.persisted? && !Spree::User.exists?(email: resource.email) + new_user_reset_path(resource) + else + spree.admin_path + end + end +end diff --git a/spec/controllers/enterprise_confirmations_controller_spec.rb b/spec/controllers/enterprise_confirmations_controller_spec.rb index fa7746df36..6fdab61be2 100644 --- a/spec/controllers/enterprise_confirmations_controller_spec.rb +++ b/spec/controllers/enterprise_confirmations_controller_spec.rb @@ -4,25 +4,58 @@ describe EnterpriseConfirmationsController do include AuthenticationWorkflow let!(:user) { create_enterprise_user( enterprise_limit: 10 ) } let!(:unconfirmed_enterprise) { create(:distributor_enterprise, confirmed_at: nil, owner: user) } - let!(:confirmed_enterprise) { create(:distributor_enterprise, owner: user) } + let!(:confirmed_enterprise) { create(:distributor_enterprise, confirmed_at: nil, owner: user) } + let!(:confirmed_token) { confirmed_enterprise.confirmation_token } let!(:unowned_enterprise) { create(:distributor_enterprise) } before do controller.stub spree_current_user: user @request.env["devise.mapping"] = Devise.mappings[:enterprise] + confirmed_enterprise.confirm! end context "confirming an enterprise" do - it "that has already been confirmed" do - spree_get :show, confirmation_token: confirmed_enterprise.confirmation_token - expect(response).to redirect_to spree.admin_path - expect(flash[:error]).to eq I18n.t('devise.enterprise_confirmations.enterprise.not_confirmed') + context "that has already been confirmed" do + + before do + spree_get :show, confirmation_token: confirmed_token + end + + it "redirects the user to admin" do + expect(response).to redirect_to spree.admin_path + expect(flash[:error]).to eq I18n.t('devise.enterprise_confirmations.enterprise.not_confirmed') + end end - it "that has not already been confirmed" do - spree_get :show, confirmation_token: unconfirmed_enterprise.confirmation_token - expect(response).to redirect_to spree.admin_path - expect(flash[:success]).to eq I18n.t('devise.enterprise_confirmations.enterprise.confirmed') + context "that has not been confirmed" do + context "where the enterprise contact email maps to an existing user account" do + before do + unconfirmed_enterprise.update_attribute(:email, user.email) + end + + it "redirects the user to admin" do + spree_get :show, confirmation_token: unconfirmed_enterprise.confirmation_token + expect(response).to redirect_to spree.admin_path + expect(flash[:success]).to eq I18n.t('devise.enterprise_confirmations.enterprise.confirmed') + end + end + + context "where the enterprise contact email doesn't map to an existing user account" do + let(:new_user) { create_enterprise_user } + + before do + unconfirmed_enterprise.update_attribute(:email, 'random@email.com') + allow(Spree::User).to receive(:create) { new_user } + allow(new_user).to receive(:reset_password_token) { "token" } + end + + it "redirects to the user to reset their password" do + spree_get :show, confirmation_token: unconfirmed_enterprise.confirmation_token + expect(response).to redirect_to spree.edit_spree_user_password_path(new_user, :reset_password_token => "token", return_to: spree.admin_path) + expect(flash[:success]).to eq I18n.t('devise.enterprise_confirmations.enterprise.confirmed') + expect(unconfirmed_enterprise.users(:reload)).to include new_user + end + end end end @@ -39,4 +72,4 @@ describe EnterpriseConfirmationsController do expect(flash[:error]).to eq "Authorization Failure" end end -end \ No newline at end of file +end From 524f02717b7df11aec946fe7db000d3a4ddfd9d1 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 17 Apr 2015 10:25:38 +1000 Subject: [PATCH 50/81] Don't redirect to root when closing login window on checkout page --- .../darkswarm/services/authentication_service.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee b/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee index f2d7049e67..c83022b579 100644 --- a/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee @@ -25,7 +25,7 @@ Darkswarm.factory "AuthenticationService", (Navigation, $modal, $location, Redir active: Navigation.active close: -> - if location.pathname == "/" + if location.pathname in ["/", "/checkout"] Navigation.navigate "/" else Loading.message = "Taking you back to the home page" From 2b5fc656fece74de3b9b5b5f67bd8e7e9776ae99 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 17 Apr 2015 10:40:25 +1000 Subject: [PATCH 51/81] Configure delayed job logging, add startup script for monit --- config/initializers/delayed_job.rb | 1 + script/delayed_job.sh | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 config/initializers/delayed_job.rb create mode 100644 script/delayed_job.sh diff --git a/config/initializers/delayed_job.rb b/config/initializers/delayed_job.rb new file mode 100644 index 0000000000..b95d718c6d --- /dev/null +++ b/config/initializers/delayed_job.rb @@ -0,0 +1 @@ +Delayed::Worker.logger = Logger.new(Rails.root.join('log', 'delayed_job.log')) diff --git a/script/delayed_job.sh b/script/delayed_job.sh new file mode 100644 index 0000000000..a1498d3ce6 --- /dev/null +++ b/script/delayed_job.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +export HOME="/home/openfoodweb" +export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH" + +$HOME/apps/openfoodweb/current/script/delayed_job $@ From 0cf8b017b9967c47b31a7d04011b98e00cde13e0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 17 Apr 2015 13:20:01 +1000 Subject: [PATCH 52/81] Pulling out taxons and properties placeholders from producer and product modals --- .../partials/enterprise_details.html.haml | 33 +++++++-------- .../templates/product_modal.html.haml | 40 ++++++++++--------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/app/assets/javascripts/templates/partials/enterprise_details.html.haml b/app/assets/javascripts/templates/partials/enterprise_details.html.haml index 5593976838..3a9d97f59e 100644 --- a/app/assets/javascripts/templates/partials/enterprise_details.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_details.html.haml @@ -1,27 +1,28 @@ -.row{bindonce: true} +.row{bindonce: true} .small-12.large-8.columns / TODO: Rob add logic for taxons and properties too: / %div{"ng-if" => "enterprise.long_description.length > 0 || enterprise.logo"} %div %p.modal-header About / TODO: Rob - add in taxons and properties and property pop-overs - - %div - %span.filter-shopfront.taxon-selectors - %ul.inline-block - %li - %a.button.tiny.disabled Grains - %li - %a.button.tiny.disabled Dairy - %span.filter-shopfront.property-selectors.pad-top - %ul.inline-block - %li - %a.button.tiny Organic certified - / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ - + -# TODO: Add producer taxons and properties here + -# %div + -# %span.filter-shopfront.taxon-selectors + -# %ul.inline-block + -# %li + -# %a.button.tiny.disabled Grains + -# %li + -# %a.button.tiny.disabled Dairy + -# + -# %span.filter-shopfront.property-selectors.pad-top + -# %ul.inline-block + -# %li + -# %a.button.tiny Organic certified + -# / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ + -# .about-container.pad-top - %img.enterprise-logo{"bo-src" => "enterprise.logo", "bo-if" => "enterprise.logo"} + %img.enterprise-logo{"bo-src" => "enterprise.logo", "bo-if" => "enterprise.logo"} %p.text-small{"ng-bind-html" => "enterprise.long_description"} .small-12.large-4.columns %ng-include{src: "'partials/contact.html'"} diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index b812424cc7..1b59838e53 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -1,28 +1,30 @@ .row - + .columns.small-12.large-6.product-header %h3 {{product.name}} - %span + %span %em from %span.avenir {{ enterprise.name }} - - / TODO: Rob - add in taxons and properties and property pop-overs - / %render-svg{path: "{{product.primary_taxon.icon}}"} - .pad-top - %span.filter-shopfront.taxon-selectors - %ul.inline-block - %li - %a.button.tiny.disabled Grains - %li - %a.button.tiny.disabled Dairy - %span.filter-shopfront.property-selectors.pad-top - %ul.inline-block - %li - %a.button.tiny Organic certified - / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ - - + + -# TODO: Add product taxons and properties here + -# / TODO: Rob - add in taxons and properties and property pop-overs + -# / %render-svg{path: "{{product.primary_taxon.icon}}"} + -# .pad-top + -# %span.filter-shopfront.taxon-selectors + -# %ul.inline-block + -# %li + -# %a.button.tiny.disabled Grains + -# %li + -# %a.button.tiny.disabled Dairy + -# + -# %span.filter-shopfront.property-selectors.pad-top + -# %ul.inline-block + -# %li + -# %a.button.tiny Organic certified + -# / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ + + %div{"ng-if" => "product.description"} %hr %p.text-small {{product.description}} From b44f2bcdf5d0be0798e7729c520b2165ce176f85 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 17 Apr 2015 13:14:33 +1000 Subject: [PATCH 53/81] Fixing trial expiry test --- spec/controllers/admin/enterprises_controller_spec.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 75a0dc1a4b..bb77007015 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -277,13 +277,9 @@ module Admin end context "if the trial has finished" do - before do - enterprise.shop_trial_start_date = (Date.today - 30.days).to_time - enterprise.save! - end - it "is disallowed" do Timecop.freeze(Time.zone.local(2015, 4, 16, 14, 0, 0)) do + enterprise.update_attribute(:shop_trial_start_date, 30.days.ago.beginning_of_day) spree_post :set_sells, { id: enterprise, sells: 'own' } expect(response).to redirect_to spree.admin_path trial_expiry = Date.today.strftime("%Y-%m-%d") From e75c6a8e1d82340756899b159a2db757301782f7 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 11:20:47 +1100 Subject: [PATCH 54/81] Add helper to display total tax on an order --- app/helpers/checkout_helper.rb | 14 +++++++++----- spec/helpers/checkout_helper_spec.rb | 8 ++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index cd986eb565..c1139b63f9 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -18,20 +18,24 @@ module CheckoutHelper end def display_checkout_admin_and_handling_adjustments_total_for(order) - adjustments = order.adjustments.eligible.where('originator_type = ? AND source_type != ? ', 'EnterpriseFee', 'Spree::LineItem' ) - Spree::Money.new( adjustments.sum( &:amount ) , { :currency => order.currency }) + adjustments = order.adjustments.eligible.where('originator_type = ? AND source_type != ? ', 'EnterpriseFee', 'Spree::LineItem') + Spree::Money.new adjustments.sum(&:amount) , currency: order.currency end def checkout_line_item_adjustments(order) - order.adjustments.eligible.where( source_type: "Spree::LineItem") + order.adjustments.eligible.where(source_type: "Spree::LineItem") end def checkout_subtotal(order) - order.item_total + checkout_line_item_adjustments(order).sum( &:amount ) + order.item_total + checkout_line_item_adjustments(order).sum(&:amount) end def display_checkout_subtotal(order) - Spree::Money.new( checkout_subtotal(order) , { :currency => order.currency }) + Spree::Money.new checkout_subtotal(order) , currency: order.currency + end + + def display_checkout_tax_total(order) + Spree::Money.new order.total_tax, currency: order.currency end def checkout_state_options(source_address) diff --git a/spec/helpers/checkout_helper_spec.rb b/spec/helpers/checkout_helper_spec.rb index c4979be40d..e47a5e8c51 100644 --- a/spec/helpers/checkout_helper_spec.rb +++ b/spec/helpers/checkout_helper_spec.rb @@ -12,4 +12,12 @@ describe CheckoutHelper do helper.validated_input("test", "foo", type: :email) end + + describe "displaying the tax total for an order" do + let(:order) { double(:order, total_tax: 123.45, currency: 'AUD') } + + it "retrieves the total tax on the order" do + helper.display_checkout_tax_total(order).should == Spree::Money.new(123.45, currency: 'AUD') + end + end end From 210c76eddc23eeee7bf9346e967b343df033797f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 11:22:12 +1100 Subject: [PATCH 55/81] Checkout won't load without payment and shipping methods; move spec to context where it can test the checkout --- spec/features/admin/reports_spec.rb | 2 +- spec/features/consumer/shopping/checkout_spec.rb | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index f16b736c00..1d58938e38 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -108,7 +108,7 @@ feature %q{ page.should have_content 'Payment State' end - describe "Sales tax report" do + describe "sales tax report" do let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true) } let(:distributor2) { create(:distributor_enterprise, with_payment_and_shipping: true) } let(:user1) { create_enterprise_user enterprises: [distributor1] } diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 12eca09339..290335f09e 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -21,11 +21,6 @@ feature "As a consumer I want to check out my cart", js: true do add_product_to_cart end - it "shows the current distributor on checkout" do - visit checkout_path - page.should have_content distributor.name - end - describe "with shipping and payment methods" do let(:sm1) { create(:shipping_method, require_ship_address: true, name: "Frogs", description: "yellow", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0.00)) } let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 4.56)) } @@ -50,6 +45,11 @@ feature "As a consumer I want to check out my cart", js: true do checkout_as_guest end + it "shows the current distributor" do + visit checkout_path + page.should have_content distributor.name + end + it "shows a breakdown of the order price" do toggle_shipping choose sm2.name From 68f0e51c028df88a61485adb4be72d87da022007 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 11:23:04 +1100 Subject: [PATCH 56/81] Change add_product_to_cart spec helper to use OrderPopulator, reducing inconsistencies in order adjustments, tax etc. --- spec/features/consumer/shopping/checkout_auth_spec.rb | 2 +- spec/features/consumer/shopping/checkout_spec.rb | 2 +- spec/support/request/shop_workflow.rb | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index aaf543a7fa..8b4f3bb977 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -9,7 +9,7 @@ feature "As a consumer I want to check out my cart", js: true do let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } 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, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } let(:product) { create(:simple_product, supplier: supplier) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 290335f09e..a17f7910ec 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -12,7 +12,7 @@ feature "As a consumer I want to check out my cart", js: true do let(:supplier) { create(:supplier_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(:product) { create(:simple_product, supplier: supplier, price: 10) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } before do diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index 8f088095e8..a0e6ab846c 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -17,11 +17,10 @@ module ShopWorkflow end def add_product_to_cart - create(:line_item, variant: product.master, order: order) - order.reload + populator = Spree::OrderPopulator.new(order, order.currency) + populator.populate(variants: {product.master.id => 1}) - # Recalculate totals - order.save! + # Recalculate fee totals order.update_distribution_charge! end From beec910445490949a3f1911f3e3efc65b0a8a325 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 11:23:28 +1100 Subject: [PATCH 57/81] Display tax in cart --- app/views/spree/orders/_form.html.haml | 6 ++++ spec/features/consumer/shopping/cart_spec.rb | 32 ++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 spec/features/consumer/shopping/cart_spec.rb diff --git a/app/views/spree/orders/_form.html.haml b/app/views/spree/orders/_form.html.haml index bba45a53c8..fe46e06fd2 100644 --- a/app/views/spree/orders/_form.html.haml +++ b/app/views/spree/orders/_form.html.haml @@ -56,3 +56,9 @@ %td.text-right %h5.order-total.grand-total= @order.display_total %td + + %tr + %td.text-right{colspan:"3"} (includes tax) + %td.text-right + %span.order-total.tax-total= display_checkout_tax_total(@order) + %td diff --git a/spec/features/consumer/shopping/cart_spec.rb b/spec/features/consumer/shopping/cart_spec.rb new file mode 100644 index 0000000000..1420784c52 --- /dev/null +++ b/spec/features/consumer/shopping/cart_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +feature "full-page cart", js: true do + include AuthenticationWorkflow + include WebHelper + include ShopWorkflow + include UIComponentHelper + + describe "viewing the cart" do + describe "tax" do + let!(:zone) { create(:zone_with_member) } + let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let(:supplier) { create(:supplier_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: 11.00, tax_category: product.tax_category) } + let(:product) { create(:taxed_product, supplier: supplier, zone: zone, price: 110.00, tax_rate_amount: 0.1) } + let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } + + before do + add_enterprise_fee enterprise_fee + set_order order + add_product_to_cart + visit spree.cart_path + end + + it "shows the total tax for the order, including product tax and tax on fees" do + save_screenshot '/home/rohan/ss.png', full: true + page.should have_selector '.tax-total', text: '11.00' # 10 + 1 + end + end + end +end From 829d11d4b2d14cf4fc3a3e43647b6cebb9124d9c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 12:33:55 +1100 Subject: [PATCH 58/81] Deliver a warning if attempting to create an invalid taxed_product from factory --- spec/factories.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/factories.rb b/spec/factories.rb index aa86af8563..c798999134 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -200,6 +200,7 @@ FactoryGirl.define do tax_category { create(:tax_category) } after(:create) do |product, proxy| + raise "taxed_product factory requires a zone" unless proxy.zone create(:tax_rate, amount: proxy.tax_rate_amount, tax_category: product.tax_category, included_in_price: true, calculator: Spree::Calculator::DefaultTax.new, zone: proxy.zone) end end From 6bb926f81109876ea6bd38a77540239ffc0e8e99 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 12:34:14 +1100 Subject: [PATCH 59/81] Do not show tax in checkout --- app/views/checkout/_summary.html.haml | 5 ----- spec/features/consumer/shopping/checkout_spec.rb | 8 +++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index 5bdd13187b..1d24d1dcf2 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -19,11 +19,6 @@ %tr %th Total %td.total.text-right {{ Checkout.cartTotal() | localizeCurrency }} - - 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", "ofn-focus" => "accordion['payment']" %a.button.secondary{href: cart_url} diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index a17f7910ec..6e7ff19549 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -8,11 +8,12 @@ feature "As a consumer I want to check out my cart", js: true do include WebHelper include UIComponentHelper + let!(:zone) { create(:zone_with_member) } let(:distributor) { create(:distributor_enterprise) } let(:supplier) { create(:supplier_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, price: 10) } + let(:product) { create(:taxed_product, supplier: supplier, price: 10, zone: zone, tax_rate_amount: 0.1) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } before do @@ -57,6 +58,11 @@ feature "As a consumer I want to check out my cart", js: true do 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" + + # Tax should not be displayed in checkout, as the customer's choice of shipping method + # affects the tax and we haven't written code to live-update the tax amount when they + # make a change. + page.should_not have_content product.tax_category.name end it "shows all shipping methods, but doesn't show ship address when not needed" do From 77d72552432a6c487ef1506211c959609cf9e2a3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 14:50:52 +1100 Subject: [PATCH 60/81] Use short syntax for rendering partials --- app/views/spree/orders/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/spree/orders/show.html.haml b/app/views/spree/orders/show.html.haml index f9d2830486..82120add82 100644 --- a/app/views/spree/orders/show.html.haml +++ b/app/views/spree/orders/show.html.haml @@ -9,7 +9,7 @@ - else = @order.distributor.next_collection_at - = render partial: "shopping_shared/details" + = render "shopping_shared/details" %fieldset#order_summary{"data-hook" => ""} .row @@ -22,7 +22,7 @@ - if params.has_key? :checkout_complete %h1= t(:thank_you_for_your_order) - = render :partial => 'spree/shared/order_details', :locals => { :order => @order } + = render 'spree/shared/order_details', order: @order .row .columns.large-12 From 3ce2c5b84faeeb712f23d5f581f1faf6c8fdb54c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 14:53:57 +1100 Subject: [PATCH 61/81] Show tax on order confirmation page --- .../spree/shared/_order_details.html.haml | 63 +++++++++---------- .../consumer/shopping/checkout_spec.rb | 16 ++++- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/app/views/spree/shared/_order_details.html.haml b/app/views/spree/shared/_order_details.html.haml index da7bc84ca1..4fe07f07aa 100644 --- a/app/views/spree/shared/_order_details.html.haml +++ b/app/views/spree/shared/_order_details.html.haml @@ -130,37 +130,36 @@ %td.text-right.total{"data-hook" => "order_item_total"} %span= item.display_amount_with_adjustments.to_html - %tfoot#order-total{"data-hook" => "order_details_total"} - %tr.total - %td.text-right{colspan: "3"} - %h5 - Total - %td.text-right.total - %h5#order_total= order.display_total.to_html - - - if order.price_adjustment_totals.present? - %tfoot#price-adjustments{"data-hook" => "order_details_price_adjustments"} - - order.price_adjustment_totals.each do |key, total| - %tr.total - %td.text-right{colspan: "3"} - %strong - = key - %td.text-right.total - %span= total - - %tfoot#subtotal{"data-hook" => "order_details_subtotal"} - %tr#subtotal-row.total - %td.text-right{colspan: "3"} - %strong - Produce - %td.text-right.total - %span= display_checkout_subtotal(order) - - %tfoot#order-charges{"data-hook" => "order_details_adjustments"} - - checkout_adjustments_for(order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| - %tr.total - %td.text-right{:colspan => "3"} + %tfoot + #subtotal{"data-hook" => "order_details_subtotal"} + %tr#subtotal-row.total + %td.text-right{colspan: "3"} %strong - = adjustment.label + Produce %td.text-right.total - %span= adjustment.display_amount.to_html + %span= display_checkout_subtotal(order) + + #order-charges{"data-hook" => "order_details_adjustments"} + - checkout_adjustments_for(order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + %tr.total + %td.text-right{:colspan => "3"} + %strong + = adjustment.label + %td.text-right.total + %span= adjustment.display_amount.to_html + + #order-total{"data-hook" => "order_details_total"} + %tr.total + %td.text-right{colspan: "3"} + %h5 + Total + %td.text-right.total + %h5#order_total= order.display_total.to_html + + - if order.total_tax > 0 + #tax{"data-hook" => "order_details_tax"} + %tr#tax-row.total + %td.text-right{colspan: "3"} + (includes tax) + %td.text-right.total + %span= display_checkout_tax_total(order) diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 6e7ff19549..85c1c446be 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -12,11 +12,14 @@ 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, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } - let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23) } + let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23, tax_category: product.tax_category) } let(:product) { create(:taxed_product, supplier: supplier, price: 10, zone: zone, tax_rate_amount: 0.1) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } before do + Spree::Config.shipment_inc_vat = true + Spree::Config.shipping_tax_rate = 0.25 + add_enterprise_fee enterprise_fee set_order order add_product_to_cart @@ -131,6 +134,17 @@ feature "As a consumer I want to check out my cart", js: true do o = Spree::Order.complete.first expect(o.special_instructions).to eq "SpEcIaL NoTeS" + + # The Spree tax summary should not be displayed + page.should_not have_content product.tax_category.name + + # The total tax for the order, including shipping and fee tax, should be displayed + # product tax ($10.00 @ 10% = $0.91) + # + fee tax ($ 1.23 @ 10% = $0.11) + # + shipping tax ($ 4.56 @ 25% = $0.91) + # = $1.93 + page.should have_content "(includes tax)" + page.should have_content "$1.93" end context "with basic details filled" do From 291499044449283895e8934ed62a15e31d8d3fee Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 15:59:33 +1100 Subject: [PATCH 62/81] Add save_and_open spec helper method to open HTML emails in the browser --- spec/spec_helper.rb | 1 + spec/support/html_helper.rb | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 spec/support/html_helper.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d5410a5026..2bf1063dca 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -92,6 +92,7 @@ RSpec.configure do |config| config.include OpenFoodNetwork::FeatureToggleHelper config.include OpenFoodNetwork::EnterpriseGroupsHelper config.include OpenFoodNetwork::DistributionHelper + config.include OpenFoodNetwork::HtmlHelper config.include ActionView::Helpers::DateHelper config.include OpenFoodNetwork::DelayedJobHelper diff --git a/spec/support/html_helper.rb b/spec/support/html_helper.rb new file mode 100644 index 0000000000..54c6ffe1f1 --- /dev/null +++ b/spec/support/html_helper.rb @@ -0,0 +1,10 @@ +module OpenFoodNetwork + module HtmlHelper + def save_and_open(html) + require "launchy" + file = Tempfile.new('html') + file.write html + Launchy.open(file.path) + end + end +end From 4bd1ff20111237f1a103b4098f7d95faca5c1885 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 16:00:48 +1100 Subject: [PATCH 63/81] Extract the bulk of the confirmation emails into partials, eliminating a lot of duplication --- .../order_mailer/_order_summary.html.haml | 46 ++++++ .../spree/order_mailer/_payment.html.haml | 14 ++ .../spree/order_mailer/_shipping.html.haml | 59 ++++++++ .../_special_instructions.html.haml | 7 + .../confirm_email_for_customer.html.haml | 133 +----------------- .../confirm_email_for_shop.html.haml | 132 +---------------- 6 files changed, 134 insertions(+), 257 deletions(-) create mode 100644 app/views/spree/order_mailer/_order_summary.html.haml create mode 100644 app/views/spree/order_mailer/_payment.html.haml create mode 100644 app/views/spree/order_mailer/_shipping.html.haml create mode 100644 app/views/spree/order_mailer/_special_instructions.html.haml diff --git a/app/views/spree/order_mailer/_order_summary.html.haml b/app/views/spree/order_mailer/_order_summary.html.haml new file mode 100644 index 0000000000..0e68c38c9b --- /dev/null +++ b/app/views/spree/order_mailer/_order_summary.html.haml @@ -0,0 +1,46 @@ +%table.order-summary{:width => "100%"} + %thead + %tr + %th{:align => "left"} + %h4 Item + %th{:align => "right", :width => "25%"} + %h4 Qty + %th{:align => "right", :width => "25%"} + %h4 Price + %tbody + - @order.line_items.each do |item| + %tr + %td + - if item.variant.product.name == item.variant.name_to_display + %strong= "#{raw(item.variant.product.name)}" + - else + %strong + %span= "#{raw(item.variant.product.name)}" + %span= "- " + "#{raw(item.variant.name_to_display)}" + - if item.variant.options_text + = "(" + "#{raw(item.variant.options_text)}" + ")" + %br + %small + %em= raw(item.variant.product.supplier.name) + %td{:align => "right"} + = item.quantity + %td{:align => "right"} + = item.display_amount_with_adjustments + %tfoot + %tr + %td{:align => "right", :colspan => "2"} + Subtotal: + %td{:align => "right"} + = display_checkout_subtotal(@order) + - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + %tr + %td{:align => "right", :colspan => "2"} + = "#{raw(adjustment.label)}:" + %td{:align => "right"} + = adjustment.display_amount + %tr + %td{:align => "right", :colspan => "2"} + %strong Total: + %td{:align => "right"} + %strong= @order.display_total +%p   diff --git a/app/views/spree/order_mailer/_payment.html.haml b/app/views/spree/order_mailer/_payment.html.haml new file mode 100644 index 0000000000..41968ba1ad --- /dev/null +++ b/app/views/spree/order_mailer/_payment.html.haml @@ -0,0 +1,14 @@ +- if @order.payments.first.andand.payment_method.andand.type == "Spree::PaymentMethod::Check" and @order.payments.first.andand.payment_method.andand.description + %p.callout + %span{:style => "float:right;"} + - if @order.paid? + PAID + - else + NOT PAID + %strong Payment summary + %h4 + Paying via: + %strong= @order.payments.first.andand.payment_method.andand.name.andand.html_safe + %p + %em= @order.payments.first.andand.payment_method.andand.description.andand.html_safe + %p   diff --git a/app/views/spree/order_mailer/_shipping.html.haml b/app/views/spree/order_mailer/_shipping.html.haml new file mode 100644 index 0000000000..cb9cde72ca --- /dev/null +++ b/app/views/spree/order_mailer/_shipping.html.haml @@ -0,0 +1,59 @@ +- if @order.shipping_method.andand.require_ship_address + / Delivery details + %p.callout + %strong + - if @order.shipping_method.andand.name + #{@order.shipping_method.name.html_safe} + - else + Delivery details + + - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + %h4 + Delivery on: + %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} + - if @order.shipping_method.andand.description + %p + %em #{@order.shipping_method.description.html_safe} + %br   + + - if @order.ship_address + %h4 Delivery address: + %p + #{@order.ship_address.full_name} + %br + #{@order.ship_address.full_address} + %br + #{@order.ship_address.phone} + %br   + + +- else + / Collection details + %p.callout + %strong + - if @order.shipping_method.andand.name + #{@order.shipping_method.name.html_safe} + - else + Collection details + + - if @order.order_cycle.andand.pickup_time_for(@order.distributor).present? + %h4 + Ready for collection: + %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} + + - if @order.shipping_method.andand.description.present? + %p + %em #{@order.shipping_method.description.html_safe} + %br   + + - if @order.ship_address.full_address + %p + %strong Collecting from: + %br + #{@order.ship_address.full_address} + + - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor).present? + %p + %strong Collection instructions: + %br + #{@order.order_cycle.pickup_instructions_for(@order.distributor)} diff --git a/app/views/spree/order_mailer/_special_instructions.html.haml b/app/views/spree/order_mailer/_special_instructions.html.haml new file mode 100644 index 0000000000..5145260411 --- /dev/null +++ b/app/views/spree/order_mailer/_special_instructions.html.haml @@ -0,0 +1,7 @@ +- if @order.special_instructions.present? + %br + %p + %small + %strong Your notes: + %br + #{@order.special_instructions} diff --git a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml index 4488c76edb..0816bd432d 100644 --- a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml @@ -23,135 +23,10 @@ Here are your order details from %strong= "#{@order.distributor.name}:" -%table.order-summary{:width => "100%"} - %thead - %tr - %th{:align => "left"} - %h4 Item - %th{:align => "right", :width => "25%"} - %h4 Qty - %th{:align => "right", :width => "25%"} - %h4 Price - %tbody - - @order.line_items.each do |item| - %tr - %td - - if item.variant.product.name == item.variant.name_to_display - %strong= "#{raw(item.variant.product.name)}" - - else - %strong - %span= "#{raw(item.variant.product.name)}" - %span= "- " + "#{raw(item.variant.name_to_display)}" - - if item.variant.options_text - = "(" + "#{raw(item.variant.options_text)}" + ")" - %br - %small - %em= raw(item.variant.product.supplier.name) - %td{:align => "right"} - = item.quantity - %td{:align => "right"} - = item.display_amount_with_adjustments - %tfoot - %tr - %td{:align => "right", :colspan => "2"} - Subtotal: - %td{:align => "right"} - = display_checkout_subtotal(@order) - - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| - %tr - %td{:align => "right", :colspan => "2"} - = "#{raw(adjustment.label)}:" - %td{:align => "right"} - = adjustment.display_amount - %tr - %td{:align => "right", :colspan => "2"} - %strong Total: - %td{:align => "right"} - %strong= @order.display_total -%p   - -- if @order.payments.first.andand.payment_method.andand.type == "Spree::PaymentMethod::Check" and @order.payments.first.andand.payment_method.andand.description - %p.callout - %span{:style => "float:right;"} - - if @order.paid? - PAID - - else - NOT PAID - %strong Payment summary - %h4 - Paying via: - %strong= @order.payments.first.andand.payment_method.andand.name.andand.html_safe - %p - %em= @order.payments.first.andand.payment_method.andand.description.andand.html_safe - %p   - -- if @order.shipping_method.andand.require_ship_address - / Delivery details - %p.callout - %strong - - if @order.shipping_method.andand.name - #{@order.shipping_method.name.html_safe} - - else - Delivery details - - - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - %h4 - Delivery on: - %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - - if @order.shipping_method.andand.description - %p - %em #{@order.shipping_method.description.html_safe} - %br   - - - if @order.ship_address - %h4 Delivery address: - %p - #{@order.ship_address.full_name} - %br - #{@order.ship_address.full_address} - %br - #{@order.ship_address.phone} - %br   - - -- else - / Collection details - %p.callout - %strong - - if @order.shipping_method.andand.name - #{@order.shipping_method.name.html_safe} - - else - Collection details - - - if @order.order_cycle.andand.pickup_time_for(@order.distributor).present? - %h4 - Ready for collection: - %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - - - if @order.shipping_method.andand.description.present? - %p - %em #{@order.shipping_method.description.html_safe} - %br   - - - if @order.ship_address.full_address - %p - %strong Collecting from: - %br - #{@order.ship_address.full_address} - - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor).present? - %p - %strong Collection instructions: - %br - #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - -- if @order.special_instructions.present? - %br - %p - %small - %strong Your notes: - %br - #{@order.special_instructions} += render 'order_summary' += render 'payment' += render 'shipping' += render 'special_instructions' %br %p.callout diff --git a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml index fe48c1c6d1..f62b3cdfb4 100644 --- a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml @@ -23,134 +23,10 @@ %strong= "#{@order.bill_address.firstname} #{@order.bill_address.lastname}" completed the following order at your shopfront: -%table.order-summary{:width => "100%"} - %thead - %tr - %th{:align => "left"} - %h4 Item - %th{:align => "right", :width => "25%"} - %h4 Qty - %th{:align => "right", :width => "25%"} - %h4 Price - %tbody - - @order.line_items.each do |item| - %tr - %td - - if item.variant.product.name == item.variant.name_to_display - %strong= "#{raw(item.variant.product.name)}" - - else - %strong - %span= "#{raw(item.variant.product.name)}" - %span= "- " + "#{raw(item.variant.name_to_display)}" - - if item.variant.options_text - = "(" + "#{raw(item.variant.options_text)}" + ")" - %br - %small - %em= raw(item.variant.product.supplier.name) - %td{:align => "right"} - = item.quantity - %td{:align => "right"} - = item.display_amount_with_adjustments - %tfoot - %tr - %td{:align => "right", :colspan => "2"} - Subtotal: - %td{:align => "right"} - = display_checkout_subtotal(@order) - - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| - %tr - %td{:align => "right", :colspan => "2"} - = "#{raw(adjustment.label)}:" - %td{:align => "right"} - = adjustment.display_amount - %tr - %td{:align => "right", :colspan => "2"} - %strong Total: - %td{:align => "right"} - %strong= @order.display_total -%p   - -- if @order.payments.first.andand.payment_method.andand.type == "Spree::PaymentMethod::Check" and @order.payments.first.andand.payment_method.andand.description - %p.callout - %span{:style => "float:right;"} - - if @order.paid? - PAID - - else - NOT PAID - %strong Payment summary - %h4 - Paying via: - %strong= @order.payments.first.andand.payment_method.andand.name.andand.html_safe - %p - %em= @order.payments.first.andand.payment_method.andand.description.andand.html_safe - %p   - -- if @order.shipping_method.andand.require_ship_address - / Delivery details - %p.callout - %strong - - if @order.shipping_method.andand.name - #{@order.shipping_method.name.html_safe} - - else - Delivery details - - - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - %h4 - Delivery on: - %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - - if @order.shipping_method.andand.description - %p - %em #{@order.shipping_method.description.html_safe} - %br   - - - if @order.ship_address - %h4 Delivery address: - %p - #{@order.ship_address.full_name} - %br - #{@order.ship_address.full_address} - %br - #{@order.ship_address.phone} - %br   - -- else - / Collection details - %p.callout - %strong - - if @order.shipping_method.andand.name - #{@order.shipping_method.name.html_safe} - - else - Collection details - - - if @order.order_cycle.andand.pickup_time_for(@order.distributor).present? - %h4 - Ready for collection: - %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - - - if @order.shipping_method.andand.description.present? - %p - %em #{@order.shipping_method.description.html_safe} - %br   - - - if @order.ship_address.full_address - %p - %strong Collecting from: - %br - #{@order.ship_address.full_address} - - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor).present? - %p - %strong Collection instructions: - %br - #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - -- if @order.special_instructions.present? - %br - %p - %small - %strong Customer notes: - %br - #{@order.special_instructions} += render 'order_summary' += render 'payment' += render 'shipping' += render 'special_instructions' %p   = render 'shared/mailers/signoff' From a93633a626658bef5161294ca9368ae9a6c7d6d9 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 16:02:55 +1100 Subject: [PATCH 64/81] Show tax on order confirmation emails --- .../order_mailer/_order_summary.html.haml | 6 ++++++ .../consumer/shopping/checkout_spec.rb | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/views/spree/order_mailer/_order_summary.html.haml b/app/views/spree/order_mailer/_order_summary.html.haml index 0e68c38c9b..acd3f39a1b 100644 --- a/app/views/spree/order_mailer/_order_summary.html.haml +++ b/app/views/spree/order_mailer/_order_summary.html.haml @@ -43,4 +43,10 @@ %strong Total: %td{:align => "right"} %strong= @order.display_total + + %tr + %td{:align => "right", :colspan => "2"} + (includes tax): + %td{:align => "right"} + = display_checkout_tax_total(@order) %p   diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 85c1c446be..a02d137983 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -126,25 +126,37 @@ feature "As a consumer I want to check out my cart", js: true do choose pm1.name end - expect do place_order page.should have_content "Your order has been processed successfully" end.to enqueue_job ConfirmOrderJob + # And the order's special instructions should be set o = Spree::Order.complete.first expect(o.special_instructions).to eq "SpEcIaL NoTeS" - # The Spree tax summary should not be displayed + # And the Spree tax summary should not be displayed page.should_not have_content product.tax_category.name - # The total tax for the order, including shipping and fee tax, should be displayed + # And the total tax for the order, including shipping and fee tax, should be displayed # product tax ($10.00 @ 10% = $0.91) # + fee tax ($ 1.23 @ 10% = $0.11) # + shipping tax ($ 4.56 @ 25% = $0.91) # = $1.93 page.should have_content "(includes tax)" page.should have_content "$1.93" + + + ActionMailer::Base.deliveries.each do |email| + email.subject.should include "#{Spree::Config.site_name} Order Confirmation" + + # The Spree tax summary should not be included in the confirmation email + email.body.should_not include product.tax_category.name + + # The total tax should also be included in the confirmation email + email.body.include?("(includes tax)").should be_true + email.body.include?("$1.93").should be_true + end end context "with basic details filled" do From 08fef890ecb059402d6fcce743bb9f54913ffd1a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 2 Apr 2015 16:07:19 +1100 Subject: [PATCH 65/81] Do not show tax lines if there is no tax on the order --- app/views/spree/order_mailer/_order_summary.html.haml | 11 ++++++----- app/views/spree/orders/_form.html.haml | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/views/spree/order_mailer/_order_summary.html.haml b/app/views/spree/order_mailer/_order_summary.html.haml index acd3f39a1b..86b1af1f3b 100644 --- a/app/views/spree/order_mailer/_order_summary.html.haml +++ b/app/views/spree/order_mailer/_order_summary.html.haml @@ -44,9 +44,10 @@ %td{:align => "right"} %strong= @order.display_total - %tr - %td{:align => "right", :colspan => "2"} - (includes tax): - %td{:align => "right"} - = display_checkout_tax_total(@order) + - if @order.total_tax > 0 + %tr + %td{:align => "right", :colspan => "2"} + (includes tax): + %td{:align => "right"} + = display_checkout_tax_total(@order) %p   diff --git a/app/views/spree/orders/_form.html.haml b/app/views/spree/orders/_form.html.haml index fe46e06fd2..e71f92452f 100644 --- a/app/views/spree/orders/_form.html.haml +++ b/app/views/spree/orders/_form.html.haml @@ -57,8 +57,9 @@ %h5.order-total.grand-total= @order.display_total %td - %tr - %td.text-right{colspan:"3"} (includes tax) - %td.text-right - %span.order-total.tax-total= display_checkout_tax_total(@order) - %td + - if @order.total_tax > 0 + %tr + %td.text-right{colspan:"3"} (includes tax) + %td.text-right + %span.order-total.tax-total= display_checkout_tax_total(@order) + %td From 9bbc151cae3db4f012fb0d2b8cb3871b0cf2faf6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 8 Apr 2015 10:40:38 +1000 Subject: [PATCH 66/81] Admin can set enterprises to charge or not charge sales tax --- app/models/enterprise.rb | 2 +- .../enterprises/form/_business_details.html.haml | 15 ++++++++++++++- config/locales/en-GB.yml | 1 + config/locales/en.yml | 1 + ...234739_add_charges_sales_tax_to_enterprises.rb | 5 +++++ spec/features/admin/enterprises_spec.rb | 4 ++++ 6 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20150407234739_add_charges_sales_tax_to_enterprises.rb diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 97766078b1..c3d9a0f08e 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -77,7 +77,7 @@ class Enterprise < ActiveRecord::Base after_rollback :restore_permalink scope :by_name, order('name') - scope :visible, where(:visible => true) + scope :visible, where(visible: true) scope :confirmed, where('confirmed_at IS NOT NULL') scope :unconfirmed, where('confirmed_at IS NULL') scope :activated, where("confirmed_at IS NOT NULL AND sells != 'unspecified'") diff --git a/app/views/admin/enterprises/form/_business_details.html.haml b/app/views/admin/enterprises/form/_business_details.html.haml index 7fb4d2ccb3..92f727d8d3 100644 --- a/app/views/admin/enterprises/form/_business_details.html.haml +++ b/app/views/admin/enterprises/form/_business_details.html.haml @@ -3,8 +3,21 @@ = f.label :abn, 'ABN' .omega.eight.columns = f.text_field :abn, { placeholder: "eg. 99 123 456 789"} + .row .alpha.three.columns = f.label :acn, 'ACN' .omega.eight.columns - = f.text_field :acn, { placeholder: "eg. 123 456 789"} \ No newline at end of file + = f.text_field :acn, { placeholder: "eg. 123 456 789"} + +.row + .three.columns.alpha + %label= t('charges_sales_tax') + .two.columns + = f.radio_button :charges_sales_tax, true +   + = f.label :charges_sales_tax, "Yes", :value => "true" + .five.columns.omega + = f.radio_button :charges_sales_tax, false +   + = f.label :charges_sales_tax, "No", :value => "false" diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 41a02f2de2..50fbe8a6db 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -21,3 +21,4 @@ en-GB: search_by_name: Search by name... producers: UK Producers producers_join: UK producers are now welcome to join Open Food Network UK. + charges_sales_tax: Charges sales tax? diff --git a/config/locales/en.yml b/config/locales/en.yml index fe5bafa19f..fe7d3c53aa 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -18,3 +18,4 @@ en: search_by_name: Search by name or suburb... producers: Aussie Producers producers_join: Australian producers are now welcome to join the Open Food Network. + charges_sales_tax: Charges GST? diff --git a/db/migrate/20150407234739_add_charges_sales_tax_to_enterprises.rb b/db/migrate/20150407234739_add_charges_sales_tax_to_enterprises.rb new file mode 100644 index 0000000000..ba8dfa7a08 --- /dev/null +++ b/db/migrate/20150407234739_add_charges_sales_tax_to_enterprises.rb @@ -0,0 +1,5 @@ +class AddChargesSalesTaxToEnterprises < ActiveRecord::Migration + def change + add_column :enterprises, :charges_sales_tax, :boolean, null: false, default: false + end +end diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index a75125bdb5..395f882b51 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -218,6 +218,7 @@ feature %q{ click_link "Business Details" fill_in 'enterprise_abn', :with => '09812309823' fill_in 'enterprise_acn', :with => '' + choose 'Yes' # enterprise_charges_sales_tax click_link "Address" fill_in 'enterprise_address_attributes_address1', :with => '35 Ballantyne St' @@ -237,6 +238,9 @@ feature %q{ @enterprise.reload expect(@enterprise.owner).to eq user + click_link "Business Details" + page.should have_checked_field "enterprise_charges_sales_tax_true" + click_link "Payment Methods" page.should have_checked_field "enterprise_payment_method_ids_#{payment_method.id}" From 05551aa2a98184d8f6351e987ea542d814151eff Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 8 Apr 2015 12:04:31 +1000 Subject: [PATCH 67/81] Make helpers available to javascript templates --- config/initializers/js_template_helpers.rb | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 config/initializers/js_template_helpers.rb diff --git a/config/initializers/js_template_helpers.rb b/config/initializers/js_template_helpers.rb new file mode 100644 index 0000000000..c3772dc443 --- /dev/null +++ b/config/initializers/js_template_helpers.rb @@ -0,0 +1,8 @@ +# Make helpers (#t in particular) available to javascript templates +# https://github.com/pitr/angular-rails-templates/issues/45#issuecomment-43229086 + +Rails.application.assets.context_class.class_eval do + include ApplicationHelper + include ActionView::Helpers + include Rails.application.routes.url_helpers +end From 0bdb8f724130bd0577d37c81389a5ea6ba0db9e3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 8 Apr 2015 12:05:07 +1000 Subject: [PATCH 68/81] User can set whether an enterprise charges sales tax when registering --- .../javascripts/templates/registration/about.html.haml | 8 ++++++++ spec/features/consumer/registration_spec.rb | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/templates/registration/about.html.haml b/app/assets/javascripts/templates/registration/about.html.haml index 5b7d2e6df6..4343daaab7 100644 --- a/app/assets/javascripts/templates/registration/about.html.haml +++ b/app/assets/javascripts/templates/registration/about.html.haml @@ -40,6 +40,14 @@ .field %label{ for: 'enterprise_acn' } ACN: %input.chunky{ id: 'enterprise_acn', placeholder: "eg. 123 456 789", ng: { model: 'enterprise.acn' } } + .row + .small-12.columns + .field + %label{ for: 'enterprise_charges_sales_tax' }= t(:charges_sales_tax) + %input{ id: 'enterprise_charges_sales_tax_true', type: 'radio', value: 'true', ng: { model: 'enterprise.charges_sales_tax' } } + %label{ for: 'enterprise_charges_sales_tax_true' } Yes + %input{ id: 'enterprise_charges_sales_tax_false', type: 'radio', value: 'false', ng: { model: 'enterprise.charges_sales_tax' } } + %label{ for: 'enterprise_charges_sales_tax_false' } No .row.buttons.pad-top .small-12.columns diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index 354a5c37eb..25a91ae160 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -60,15 +60,17 @@ feature "Registration", js: true do fill_in 'enterprise_long_desc', with: 'Long description' fill_in 'enterprise_abn', with: '12345' fill_in 'enterprise_acn', with: '54321' + choose 'Yes' # enterprise_charges_sales_tax click_button 'Continue' - # Enterprise should be update + # Enterprise should be updated expect(page).to have_content "Let's upload some pretty pictures so your profile looks great!" e.reload expect(e.description).to eq "Short description" expect(e.long_description).to eq "Long description" expect(e.abn).to eq '12345' expect(e.acn).to eq '54321' + expect(e.charges_sales_tax).to be_true # Images # Move from logo page From a6a5fdfb3b3cf7f2f99c4e93b259ef367c3d9e67 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 8 Apr 2015 13:25:47 +1000 Subject: [PATCH 69/81] Require that the user selects whether their enterprise charges sales tax --- .../javascripts/templates/registration/about.html.haml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/templates/registration/about.html.haml b/app/assets/javascripts/templates/registration/about.html.haml index 4343daaab7..be9948b95d 100644 --- a/app/assets/javascripts/templates/registration/about.html.haml +++ b/app/assets/javascripts/templates/registration/about.html.haml @@ -44,10 +44,12 @@ .small-12.columns .field %label{ for: 'enterprise_charges_sales_tax' }= t(:charges_sales_tax) - %input{ id: 'enterprise_charges_sales_tax_true', type: 'radio', value: 'true', ng: { model: 'enterprise.charges_sales_tax' } } + %input{ id: 'enterprise_charges_sales_tax_true', type: 'radio', name: 'charges_sales_tax', value: 'true', required: true, ng: { model: 'enterprise.charges_sales_tax' } } %label{ for: 'enterprise_charges_sales_tax_true' } Yes - %input{ id: 'enterprise_charges_sales_tax_false', type: 'radio', value: 'false', ng: { model: 'enterprise.charges_sales_tax' } } + %input{ id: 'enterprise_charges_sales_tax_false', type: 'radio', name: 'charges_sales_tax', value: 'false', required: true, ng: { model: 'enterprise.charges_sales_tax' } } %label{ for: 'enterprise_charges_sales_tax_false' } No + %span.error.small-12.columns{ ng: { show: "about.charges_sales_tax.$error.required && submitted" } } + You need to make a selection. .row.buttons.pad-top .small-12.columns From 3e8801b12baf684313a59a590859a7b0eba58c1b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 9 Apr 2015 09:51:52 +1000 Subject: [PATCH 70/81] Do not charge sales tax on items or on enterprise fees when the distributor of the order does not charge sales tax --- app/models/spree/tax_rate_decorator.rb | 10 +++++++++- spec/models/spree/tax_rate_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 spec/models/spree/tax_rate_spec.rb diff --git a/app/models/spree/tax_rate_decorator.rb b/app/models/spree/tax_rate_decorator.rb index 835c001a05..330312dac4 100644 --- a/app/models/spree/tax_rate_decorator.rb +++ b/app/models/spree/tax_rate_decorator.rb @@ -1,4 +1,13 @@ Spree::TaxRate.class_eval do + class << self + def match_with_sales_tax_registration(order) + return [] unless order.distributor.charges_sales_tax + match_without_sales_tax_registration(order) + end + alias_method_chain :match, :sales_tax_registration + end + + def adjust_with_included_tax(order) adjust_without_included_tax(order) @@ -7,6 +16,5 @@ Spree::TaxRate.class_eval do a.set_absolute_included_tax! a.amount end end - alias_method_chain :adjust, :included_tax end diff --git a/spec/models/spree/tax_rate_spec.rb b/spec/models/spree/tax_rate_spec.rb new file mode 100644 index 0000000000..ba13ca4b83 --- /dev/null +++ b/spec/models/spree/tax_rate_spec.rb @@ -0,0 +1,25 @@ +module Spree + describe TaxRate do + describe "selecting tax rates to apply to an order" do + let!(:zone) { create(:zone_with_member) } + let!(:order) { create(:order, distributor: hub, bill_address: create(:address)) } + let!(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::FlatRate.new(preferred_amount: 0.1), zone: zone) } + + context "when the order's hub charges sales tax" do + let(:hub) { create(:distributor_enterprise, charges_sales_tax: true) } + + it "selects all tax rates" do + TaxRate.match(order).should == [tax_rate] + end + end + + context "when the order's hub does not charge sales tax" do + let(:hub) { create(:distributor_enterprise, charges_sales_tax: false) } + + it "selects no tax rates" do + TaxRate.match(order).should be_empty + end + end + end + end +end From 048c6a8ee88388d2fb4a5bc29d479523b3eef280 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 9 Apr 2015 10:03:04 +1000 Subject: [PATCH 71/81] Include only the bare minimum of helpers into JS template context to avoid intermittent SASS @include issues --- config/initializers/js_template_helpers.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/config/initializers/js_template_helpers.rb b/config/initializers/js_template_helpers.rb index c3772dc443..27fffc9655 100644 --- a/config/initializers/js_template_helpers.rb +++ b/config/initializers/js_template_helpers.rb @@ -2,7 +2,12 @@ # https://github.com/pitr/angular-rails-templates/issues/45#issuecomment-43229086 Rails.application.assets.context_class.class_eval do - include ApplicationHelper - include ActionView::Helpers - include Rails.application.routes.url_helpers + # include ApplicationHelper + # include ActionView::Helpers + # include Rails.application.routes.url_helpers + + # Including all of the helpers (above) has caused some intermittent CSS include issues + # (not finding mixins from an @include in sass). Therefore, we're only including the + # bare minimum here. + include ActionView::Helpers::TranslationHelper end From 0b8a619274e1ed63bbd74c1c718c367b166250ed Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 9 Apr 2015 10:52:23 +1000 Subject: [PATCH 72/81] When the order does not have a hub, all tax rates apply --- app/models/spree/tax_rate_decorator.rb | 2 +- spec/features/consumer/shopping/checkout_spec.rb | 2 +- spec/models/spree/tax_rate_spec.rb | 12 ++++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/models/spree/tax_rate_decorator.rb b/app/models/spree/tax_rate_decorator.rb index 330312dac4..42eae86b8c 100644 --- a/app/models/spree/tax_rate_decorator.rb +++ b/app/models/spree/tax_rate_decorator.rb @@ -1,7 +1,7 @@ Spree::TaxRate.class_eval do class << self def match_with_sales_tax_registration(order) - return [] unless order.distributor.charges_sales_tax + return [] if order.distributor && !order.distributor.charges_sales_tax match_without_sales_tax_registration(order) end alias_method_chain :match, :sales_tax_registration diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index a02d137983..116e52e473 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -9,7 +9,7 @@ feature "As a consumer I want to check out my cart", js: true do include UIComponentHelper let!(:zone) { create(:zone_with_member) } - let(:distributor) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise, charges_sales_tax: true) } let(:supplier) { create(:supplier_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, tax_category: product.tax_category) } diff --git a/spec/models/spree/tax_rate_spec.rb b/spec/models/spree/tax_rate_spec.rb index ba13ca4b83..25aad986a0 100644 --- a/spec/models/spree/tax_rate_spec.rb +++ b/spec/models/spree/tax_rate_spec.rb @@ -5,7 +5,7 @@ module Spree let!(:order) { create(:order, distributor: hub, bill_address: create(:address)) } let!(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::FlatRate.new(preferred_amount: 0.1), zone: zone) } - context "when the order's hub charges sales tax" do + describe "when the order's hub charges sales tax" do let(:hub) { create(:distributor_enterprise, charges_sales_tax: true) } it "selects all tax rates" do @@ -13,13 +13,21 @@ module Spree end end - context "when the order's hub does not charge sales tax" do + describe "when the order's hub does not charge sales tax" do let(:hub) { create(:distributor_enterprise, charges_sales_tax: false) } it "selects no tax rates" do TaxRate.match(order).should be_empty end end + + describe "when the order does not have a hub" do + let!(:order) { create(:order, distributor: nil, bill_address: create(:address)) } + + it "selects all tax rates" do + TaxRate.match(order).should == [tax_rate] + end + end end end end From 81324f3cc41f6db560ce99a7d484ea5c8905976a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 9 Apr 2015 11:04:54 +1000 Subject: [PATCH 73/81] Do not charge tax on shipments when distributor does not charge sales tax --- app/models/spree/shipment_decorator.rb | 6 +++++- spec/models/spree/adjustment_spec.rb | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/models/spree/shipment_decorator.rb b/app/models/spree/shipment_decorator.rb index ee9189efdd..460fae4985 100644 --- a/app/models/spree/shipment_decorator.rb +++ b/app/models/spree/shipment_decorator.rb @@ -3,7 +3,11 @@ module Spree def ensure_correct_adjustment_with_included_tax ensure_correct_adjustment_without_included_tax - adjustment.set_included_tax! Config.shipping_tax_rate if Config.shipment_inc_vat + if Config.shipment_inc_vat && (order.distributor.nil? || order.distributor.charges_sales_tax) + adjustment.set_included_tax! Config.shipping_tax_rate + else + adjustment.set_included_tax! 0 + end end alias_method_chain :ensure_correct_adjustment, :included_tax diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index e69d04da6f..0f559f60cd 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -30,7 +30,8 @@ module Spree end describe "Shipment adjustments" do - let!(:order) { create(:order, shipping_method: shipping_method) } + let!(:order) { create(:order, distributor: hub, shipping_method: shipping_method) } + let(:hub) { create(:distributor_enterprise, charges_sales_tax: true) } let!(:line_item) { create(:line_item, order: order) } let(:shipping_method) { create(:shipping_method, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) } let(:adjustment) { order.adjustments(:reload).shipping.first } @@ -80,6 +81,13 @@ module Spree adjustment.included_tax.should == 0 end + + it "records 0% tax on shipments when the distributor does not charge sales tax" do + order.distributor.update_attributes! charges_sales_tax: false + order.reload.create_shipment! + + adjustment.included_tax.should == 0 + end end end From 9e8483348ff0048e16dded9024a54e126216d7ff Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 9 Apr 2015 14:38:58 +1000 Subject: [PATCH 74/81] Fix spec - enterprise needs to charge sales tax for tax to be charged --- spec/models/spree/adjustment_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index 0f559f60cd..579965aa7a 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -96,7 +96,7 @@ module Spree let(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::DefaultTax.new, zone: zone, amount: 0.1) } let(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) } - let(:coordinator) { create(:distributor_enterprise) } + let(:coordinator) { create(:distributor_enterprise, charges_sales_tax: true) } let(:variant) { create(:variant) } let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator, coordinator_fees: [enterprise_fee], distributors: [coordinator], variants: [variant]) } let!(:order) { create(:order, order_cycle: order_cycle, distributor: coordinator) } From c8bf2071871db13fadf97fc92a5ea85818c5bb18 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 17 Apr 2015 13:14:33 +1000 Subject: [PATCH 75/81] Fixing trial expiry test --- spec/controllers/admin/enterprises_controller_spec.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 75a0dc1a4b..bb77007015 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -277,13 +277,9 @@ module Admin end context "if the trial has finished" do - before do - enterprise.shop_trial_start_date = (Date.today - 30.days).to_time - enterprise.save! - end - it "is disallowed" do Timecop.freeze(Time.zone.local(2015, 4, 16, 14, 0, 0)) do + enterprise.update_attribute(:shop_trial_start_date, 30.days.ago.beginning_of_day) spree_post :set_sells, { id: enterprise, sells: 'own' } expect(response).to redirect_to spree.admin_path trial_expiry = Date.today.strftime("%Y-%m-%d") From 6d96a7a60bccb97a78f547a80f6a8187676d40a6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 17 Apr 2015 14:43:09 +1000 Subject: [PATCH 76/81] Specifiy that allSelectors attribute on filterSelectors directive is optional --- .../javascripts/darkswarm/directives/filter_selector.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee index 5232906016..b964219925 100644 --- a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee @@ -6,7 +6,7 @@ Darkswarm.directive "filterSelector", (FilterSelectorsService)-> scope: objects: "&" activeSelectors: "=" - allSelectors: "=" + allSelectors: "=?" # Optional templateUrl: "filter_selector.html" link: (scope, elem, attr)-> From 042e076b58f942a91ad66d4185e971ad5f91221c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 17 Apr 2015 14:46:32 +1000 Subject: [PATCH 77/81] Fix tax-related spec failures --- spec/features/admin/reports_spec.rb | 8 ++++---- spec/features/consumer/shopping/cart_spec.rb | 2 +- spec/features/consumer/shopping/checkout_spec.rb | 12 ------------ 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 1d58938e38..b1685be6db 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -107,10 +107,10 @@ feature %q{ page.should have_content 'Payment State' end - + describe "sales tax report" do - let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true) } - let(:distributor2) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } + let(:distributor2) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } let(:user1) { create_enterprise_user enterprises: [distributor1] } let(:user2) { create_enterprise_user enterprises: [distributor2] } let(:shipping_method) { create(:shipping_method, name: "Shipping", description: "Expensive", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 100.55)) } @@ -140,7 +140,7 @@ feature %q{ click_link "Reports" click_link "Sales Tax" end - + it "reports" do # Then it should give me access only to managed enterprises page.should have_select 'q_distributor_id_eq', with_options: [user1.enterprises.first.name] diff --git a/spec/features/consumer/shopping/cart_spec.rb b/spec/features/consumer/shopping/cart_spec.rb index 1420784c52..7ec4c75004 100644 --- a/spec/features/consumer/shopping/cart_spec.rb +++ b/spec/features/consumer/shopping/cart_spec.rb @@ -9,7 +9,7 @@ feature "full-page cart", js: true do describe "viewing the cart" do describe "tax" do let!(:zone) { create(:zone_with_member) } - let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } let(:supplier) { create(:supplier_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: 11.00, tax_category: product.tax_category) } diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 116e52e473..bf85db2a75 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -145,18 +145,6 @@ feature "As a consumer I want to check out my cart", js: true do # = $1.93 page.should have_content "(includes tax)" page.should have_content "$1.93" - - - ActionMailer::Base.deliveries.each do |email| - email.subject.should include "#{Spree::Config.site_name} Order Confirmation" - - # The Spree tax summary should not be included in the confirmation email - email.body.should_not include product.tax_category.name - - # The total tax should also be included in the confirmation email - email.body.include?("(includes tax)").should be_true - email.body.include?("$1.93").should be_true - end end context "with basic details filled" do From 32f14bca009e62a19ff18a044b01ec4780ee605a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 17 Apr 2015 15:10:41 +1000 Subject: [PATCH 78/81] Preventing fitting of selectors when there are none --- .../directives/single_line_selectors.coffee | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee b/app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee index ed756d8fde..97e03cb689 100644 --- a/app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee +++ b/app/assets/javascripts/darkswarm/directives/single_line_selectors.coffee @@ -48,14 +48,15 @@ Darkswarm.directive 'singleLineSelectors', ($timeout, $filter) -> available -= selector.width selector.fits = true if available > 0 else - for i in [scope.allSelectors.length-1..0] - selector = scope.allSelectors[i] - if !selector.fits - continue - else - if available < 0 - selector.fits = false - available += selector.width + if scope.allSelectors.length > 0 + for i in [scope.allSelectors.length-1..0] + selector = scope.allSelectors[i] + if !selector.fits + continue + else + if available < 0 + selector.fits = false + available += selector.width scope.fitting = false scope.$watchCollection "allSelectors", -> From 46690faffbdb75d2839d770b2826d49c3bb65b5e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 17 Apr 2015 15:14:19 +1000 Subject: [PATCH 79/81] Fixing Darkswarm Angular Product Controller spec --- .../darkswarm/controllers/products_controller_spec.js.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 67aced98c6..9e4237d1e7 100644 --- a/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee @@ -5,6 +5,7 @@ describe 'ProductsCtrl', -> Products = null Cart = {} Taxons = null + Properties = null beforeEach -> module('Darkswarm') @@ -17,10 +18,11 @@ describe 'ProductsCtrl', -> order_cycle: {} Taxons: taxons: [] + Properties: {} inject ($rootScope, $controller) -> scope = $rootScope - ctrl = $controller 'ProductsCtrl', {$scope: scope, Products: Products, OrderCycle: OrderCycle, Cart: Cart, Taxons: Taxons} + ctrl = $controller 'ProductsCtrl', {$scope: scope, Products: Products, OrderCycle: OrderCycle, Cart: Cart, Taxons: Taxons, Properties: Properties} it 'fetches products from Products', -> expect(scope.Products.products).toEqual ['testy mctest'] From 0d4dbd23fd5ac5dbe8906f0e5512654b1a1b5c11 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 17 Apr 2015 15:42:42 +1000 Subject: [PATCH 80/81] Remove redundant spec line, test that sells is not changed --- spec/controllers/admin/enterprises_controller_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index bb77007015..79f72cfac3 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -272,7 +272,7 @@ module Admin context "setting 'sells' to 'own'" do before do - enterprise.sells = 'own' + enterprise.sells = 'none' enterprise.save! end @@ -284,8 +284,7 @@ module Admin expect(response).to redirect_to spree.admin_path trial_expiry = Date.today.strftime("%Y-%m-%d") expect(flash[:error]).to eq "Sorry, but you've already had a trial. Expired on: #{trial_expiry}" - expect(enterprise.reload.sells).to eq 'own' - expect(enterprise.reload.shop_trial_start_date).to eq (Date.today - 30.days).to_time + expect(enterprise.reload.sells).to eq 'none' end end end From a1673afff092b419c8d0ddad8f8c9b95dc6bfc6d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 17 Apr 2015 15:46:51 +1000 Subject: [PATCH 81/81] Allow more time for phantomjs, required by first admin spec where assets are compiled --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2bf1063dca..a911fe7742 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -33,7 +33,7 @@ require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| - options = {phantomjs_options: ['--load-images=no'], window_size: [1280, 800]} + options = {phantomjs_options: ['--load-images=no'], window_size: [1280, 800], timeout: 1.minute} # Extend poltergeist's timeout to allow ample time to use pry in browser thread #options.merge! {timeout: 5.minutes} # Enable the remote inspector: Use page.driver.debug to open a remote debugger in chrome