diff --git a/app/assets/images/collapse.png b/app/assets/images/collapse.png new file mode 100644 index 0000000000..1ac2122d8a Binary files /dev/null and b/app/assets/images/collapse.png differ diff --git a/app/assets/images/expand.png b/app/assets/images/expand.png new file mode 100644 index 0000000000..aa3fc20f39 Binary files /dev/null and b/app/assets/images/expand.png differ diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 320293f364..c6028f21aa 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -227,14 +227,19 @@ productEditModule.controller "AdminProductEditCtrl", [ $scope.updateOnHand = (product) -> - product.on_hand = $scope.onHand(product) + on_demand_variants = [] + if product.variants + on_demand_variants = (variant for id, variant of product.variants when variant.on_demand) + + unless product.on_demand || on_demand_variants.length > 0 + product.on_hand = $scope.onHand(product) $scope.onHand = (product) -> onHand = 0 if product.hasOwnProperty("variants") and product.variants instanceof Object - angular.forEach product.variants, (variant) -> - onHand = parseInt(onHand) + parseInt((if variant.on_hand > 0 then variant.on_hand else 0)) + for id, variant of product.variants + onHand = onHand + parseInt(if variant.on_hand > 0 then variant.on_hand else 0) else onHand = "error" onHand @@ -311,6 +316,10 @@ productEditModule.controller "AdminProductEditCtrl", [ Object.keys(product.variants).length > 0 + $scope.hasOnDemandVariants = (product) -> + (variant for id, variant of product.variants when variant.on_demand).length > 0 + + $scope.updateProducts = (productsToSubmit) -> $scope.displayUpdating() $http( @@ -336,12 +345,10 @@ productEditModule.controller "AdminProductEditCtrl", [ $scope.submitProducts = -> - # Pack $scope.dirtyProducts, ensuring that the correct product info is sent to the server, - # then pack $scope.products, so they will match the list returned from the server - angular.forEach $scope.dirtyProducts, (product) -> - $scope.packProduct product - angular.forEach $scope.products, (product) -> - $scope.packProduct product + # Pack pack $scope.products, so they will match the list returned from the server, + # then pack $scope.dirtyProducts, ensuring that the correct product info is sent to the server. + $scope.packProduct product for id, product of $scope.products + $scope.packProduct product for id, product of $scope.dirtyProducts productsToSubmit = filterSubmitProducts($scope.dirtyProducts) if productsToSubmit.length > 0 diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 96486a4a69..32580e15ee 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -4,6 +4,7 @@ #= require spin #= require ../shared/angular #= require ../shared/angular-resource +#= require ../shared/jquery.timeago #= require foundation #= require ./shop #= require_tree . diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index 8f21390a7f..f1fdb224b0 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -1,13 +1,4 @@ -angular.module("Shop").controller "ProductsCtrl", ($scope, $rootScope, Product) -> +angular.module("Shop").controller "ProductsCtrl", ($scope, $rootScope, Product, OrderCycle) -> $scope.data = Product.data + $scope.order_cycle = OrderCycle.order_cycle Product.update() - - - #$scope.order_cycle = OrderCycle.order_cycle - #$scope.updateProducts = -> - #$scope.products = Product.all() - #$scope.$watch "order_cycle.order_cycle_id", $scope.updateProducts - #$scope.updateProducts() - - - diff --git a/app/assets/javascripts/darkswarm/shop.js.coffee b/app/assets/javascripts/darkswarm/shop.js.coffee index 85c94211e6..1163859bc9 100644 --- a/app/assets/javascripts/darkswarm/shop.js.coffee +++ b/app/assets/javascripts/darkswarm/shop.js.coffee @@ -1,7 +1,6 @@ window.Shop = angular.module("Shop", ["ngResource", "filters"]).config ($httpProvider) -> $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') -#angular.module('Shop', ['filters']) angular.module("filters", []).filter "truncate", -> (text, length, end) -> @@ -12,3 +11,8 @@ angular.module("filters", []).filter "truncate", -> text else String(text).substring(0, length - end.length) + end + +$.timeago.settings.allowFuture = true; +angular.module("filters").filter "date_in_words", -> + (date) -> + $.timeago(date) diff --git a/app/assets/javascripts/shared/jquery.timeago.js b/app/assets/javascripts/shared/jquery.timeago.js new file mode 100644 index 0000000000..cecfaeb87d --- /dev/null +++ b/app/assets/javascripts/shared/jquery.timeago.js @@ -0,0 +1,202 @@ +/** + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * @name timeago + * @version 1.3.1 + * @requires jQuery v1.2.3+ + * @author Ryan McGeary + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) { + return inWords(timestamp); + } else if (typeof timestamp === "string") { + return inWords($.timeago.parse(timestamp)); + } else if (typeof timestamp === "number") { + return inWords(new Date(timestamp)); + } else { + return inWords($.timeago.datetime(timestamp)); + } + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowFuture: false, + localeTitle: false, + cutoff: 0, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years", + wordSeparator: " ", + numbers: [] + } + }, + inWords: function(distanceMillis) { + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + } + + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 42 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.round(days)) || + days < 45 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.round(days / 30)) || + years < 1.5 && substitute($l.year, 1) || + substitute($l.years, Math.round(years)); + + var separator = $l.wordSeparator || ""; + if ($l.wordSeparator === undefined) { separator = " "; } + return $.trim([prefix, words, suffix].join(separator)); + }, + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/\.\d+/,""); // remove milliseconds + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900 + return new Date(s); + }, + datetime: function(elem) { + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); + return $t.parse(iso8601); + }, + isTime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); + } + }); + + // functions that can be called via $(el).timeago('action') + // init is default when no action is given + // functions are called with context of a single element + var functions = { + init: function(){ + var refresh_el = $.proxy(refresh, this); + refresh_el(); + var $s = $t.settings; + if ($s.refreshMillis > 0) { + this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis); + } + }, + update: function(time){ + var parsedTime = $t.parse(time); + $(this).data('timeago', { datetime: parsedTime }); + if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString()); + refresh.apply(this); + }, + updateFromDOM: function(){ + $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) }); + refresh.apply(this); + }, + dispose: function () { + if (this._timeagoInterval) { + window.clearInterval(this._timeagoInterval); + this._timeagoInterval = null; + } + } + }; + + $.fn.timeago = function(action, options) { + var fn = action ? functions[action] : functions.init; + if(!fn){ + throw new Error("Unknown function name '"+ action +"' for timeago"); + } + // each over objects here and call the requested function + this.each(function(){ + fn.call(this, options); + }); + return this; + }; + + function refresh() { + var data = prepareData(this); + var $s = $t.settings; + + if (!isNaN(data.datetime)) { + if ( $s.cutoff == 0 || distance(data.datetime) < $s.cutoff) { + $(this).text(inWords(data.datetime)); + } + } + return this; + } + + function prepareData(element) { + element = $(element); + if (!element.data("timeago")) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if ($t.settings.localeTitle) { + element.attr("title", element.data('timeago').datetime.toLocaleString()); + } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { + element.attr("title", text); + } + } + return element.data("timeago"); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return (new Date().getTime() - date.getTime()); + } + + // fix for IE6 suckage + document.createElement("abbr"); + document.createElement("time"); +})); diff --git a/app/assets/stylesheets/darkswarm/all.scss b/app/assets/stylesheets/darkswarm/all.scss index b5d1542d11..aaec952184 100644 --- a/app/assets/stylesheets/darkswarm/all.scss +++ b/app/assets/stylesheets/darkswarm/all.scss @@ -4,7 +4,8 @@ * the top of the compiled file, but it's generally better to create a new file per style scope. *= require_self - *= require foundation *= require_tree . */ + + diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 0d6137fb3a..334c3ee6ef 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -16,7 +16,7 @@ shop distributor.details box-sizing: border-box display: block - height: 150px + min-height: 150px padding: 40px 0px 0px select width: 200px @@ -29,11 +29,18 @@ shop location font-family: "AvenirBla_IE", "AvenirBla" padding-right: 16px - ordercycle + + #distributor_title + float: left display: block - position: absolute - right: 0px - top: 40px + min-width: 350px + + ordercycle + @media all and (max-width: 768px) + float: left + padding-bottom: 12px + display: block + float: right form.custom width: 400px text-align: right @@ -65,6 +72,10 @@ shop & > .content background: none border: none + img + margin: 0px 0px 0px 40px + p + max-width: 555px & > .title, &.active > .title text-transform: uppercase line-height: 50px @@ -78,11 +89,18 @@ shop display: block padding-top: 36px table + table-layout: fixed width: 100% border-collapse: collapse border: none th line-height: 50px + &.name + width: 265px + &.notes + width: 280px + &.variant + width: 150px .notes max-width: 300px td, th @@ -92,12 +110,12 @@ shop border-right: 0px td padding: 20px 0px + &.name img + float: left + margin-right: 20px input[type=number] width: 60px margin: 0px - - - display: block float: right padding-top: 14px diff --git a/app/assets/stylesheets/darkswarm/tables.css.sass b/app/assets/stylesheets/darkswarm/tables.css.sass new file mode 100644 index 0000000000..25c089e22d --- /dev/null +++ b/app/assets/stylesheets/darkswarm/tables.css.sass @@ -0,0 +1,6 @@ +table thead tr, table tbody tr + th, td + box-sizing: border-box + padding-left: 12px + padding-right: 12px + overflow: hidden diff --git a/app/assets/stylesheets/darkswarm/typography.css.sass b/app/assets/stylesheets/darkswarm/typography.css.sass index 0bf0b39e44..a36fbbd828 100644 --- a/app/assets/stylesheets/darkswarm/typography.css.sass +++ b/app/assets/stylesheets/darkswarm/typography.css.sass @@ -22,6 +22,10 @@ h1, h2, h3, h4, h5, h6, .avenir font-family: "AvenirBla_IE", "AvenirBla" padding: 0px + +.light-grey + color: #666666 + strong.avenir font-weight: normal // Avenir is basically bold anyway diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index dde9af99f4..ca002fe08a 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -10,7 +10,9 @@ class ShopController < BaseController end def products - unless @products = current_order_cycle.andand.products_distributed_by(@distributor) + unless @products = current_order_cycle.andand + .products_distributed_by(@distributor).andand + .select(&:has_stock?) render json: "", status: 404 end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 83b599b04c..e69d1489b4 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -18,6 +18,8 @@ class Enterprise < ActiveRecord::Base delegate :latitude, :longitude, :city, :state_name, :to => :address accepts_nested_attributes_for :address + has_attached_file :logo, :styles => { :medium => "300x300>", :thumb => "100x100>" }, :default_url => "/images/:style/missing.png" + has_attached_file :promo_image, :styles => { :large => "570x380>", :thumb => "100x100>" }, :default_url => "/images/:style/missing.png" validates_presence_of :name validates_presence_of :address @@ -41,6 +43,7 @@ class Enterprise < ActiveRecord::Base .uniq } + scope :with_distributed_products_outer, joins('LEFT OUTER JOIN product_distributions ON product_distributions.distributor_id = enterprises.id'). joins('LEFT OUTER JOIN spree_products ON spree_products.id = product_distributions.product_id') diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 1377ba97ba..8d54edc189 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -92,6 +92,11 @@ Spree::Product.class_eval do def product_distribution_for(distributor) self.product_distributions.find_by_distributor_id(distributor) end + + # overriding to check self.on_demand as well + def has_stock? + has_variants? ? variants.any?(&:in_stock?) : (on_demand || master.in_stock?) + end # Build a product distribution for each distributor def build_product_distributions_for_user user diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index f430970ddc..e3850d5c78 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -12,6 +12,15 @@ Spree::Variant.class_eval do after_save :update_units + # Copied and modified from Spree::Variant + def options_text + values = self.option_values.joins(:option_type).order("#{Spree::OptionType.table_name}.position asc") + + values.map! &:presentation # This line changed + + values.to_sentence({ :words_connector => ", ", :two_words_connector => ", " }) + end + def delete_unit_option_values ovs = self.option_values.where(option_type_id: Spree::Product.all_variant_unit_option_types) self.option_values.destroy ovs @@ -75,7 +84,7 @@ Spree::Variant.class_eval do # Find the largest available unit where unit_value comes to >= 1 when expressed in it. # If there is none available where this is true, use the smallest available unit. unit = units[self.product.variant_unit].select { |scale, unit_name| - unit_value / scale > 1 + unit_value / scale >= 1 }.to_a.last unit = units[self.product.variant_unit].first if unit.nil? diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 1d4194533f..46e7494355 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -41,6 +41,16 @@ %tr{"data-hook" => "acn"} %td ACN: %td= f.text_field :acn + %tr{"data-hook" => "logo"} + %td Logo: + %td + = f.file_field :logo + = image_tag @object.logo.url + %tr{"data-hook" => "promo"} + %td Promo Image: + %td + = f.file_field :promo_image + = image_tag @object.promo_image.url %fieldset %legend Address %table diff --git a/app/views/shared/_menu.html.haml b/app/views/shared/_menu.html.haml index 0531afcdcb..65973942b3 100644 --- a/app/views/shared/_menu.html.haml +++ b/app/views/shared/_menu.html.haml @@ -1,11 +1,6 @@ %nav.top-bar %section.top-bar-section %ul.left - %li= link_to image_tag("ofn_logo_small.png"), new_landing_page_path + %li= link_to image_tag("ofn_logo_small.png"), root_path %li.divider = render partial: "shared/login" - - %ul.right - %li= link_to "Distributors", "#", :data => { "reveal-id" => "become-distributor" } - %li.divider - %li= link_to "Farmers", "#", :data => { "reveal-id" => "become-farmer" } diff --git a/app/views/shop/_order_cycles.html.haml b/app/views/shop/_order_cycles.html.haml index 60d5997cf3..5c2986bedf 100644 --- a/app/views/shop/_order_cycles.html.haml +++ b/app/views/shop/_order_cycles.html.haml @@ -20,9 +20,8 @@ "ng-change" => "changeOrderCycle()", "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}"} - %closing -#%img{src: "/icon/goes/here"} Orders close - %strong {{ order_cycle.orders_close_at | date:'EEEE MM'}} + %strong {{ order_cycle.orders_close_at | date_in_words }} diff --git a/app/views/shop/_products.html.haml b/app/views/shop/_products.html.haml index 3019d22967..d332057931 100644 --- a/app/views/shop/_products.html.haml +++ b/app/views/shop/_products.html.haml @@ -1,48 +1,69 @@ -%products{"ng-controller" => "ProductsCtrl"} +%products{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null"} + %h5 Check out when you have selected everything you want = form_for :order, :url => populate_orders_path, html: {:class => "custom"} do %input#search.text{"ng-model" => "query", placeholder: "Search"} %input.button.right{type: :submit, value: "Check Out"} %table %thead - %th{colspan: 2} Item + %th.name Item %th.notes Notes - %th Variant + %th.variant Variant %th QTY %th Bulk %th Price %tbody{"ng-repeat" => "product in data.products | filter:query"} %tr.product - %td + %td.name %img{"ng-src" => "{{ product.master.images[0].small_url }}"} - {{product.master.images[0].alt}} - %td - %h5 - {{ product.name }} - {{ product.supplier.name }} + %div + %h5 + {{ product.name }} + {{ product.supplier.name }} %td.notes {{ product.description | truncate:80 }} - %td {{ product.master.options_text }} %td - %input{type: :number, value: 0, min: 0, name: "variants[{{product.master.id}}]"} + {{ product.master.options_text }} + %span{"ng-show" => "product.variants.length > 0"} + %img.collapse{src: "/assets/collapse.png", + "ng-show" => "product.show_variants", + "ng-click" => "product.show_variants = !product.show_variants"} + + %img.expand{src: "/assets/expand.png", + "ng-show" => "!product.show_variants", + "ng-click" => "product.show_variants = !product.show_variants"} + %td + %span{"ng-show" => "(product.variants.length == 0)"} + %input{type: :number, + value: 0, + min: 0, + max: "{{product.on_demand && 9999 || product.count_on_hand }}", + name: "variants[{{product.master.id}}]", + id: "variants_{{product.master.id}}"} %td.group_buy - %span{"ng-show" => "product.group_buy"} - Available - %span{"ng-hide" => "product.group_buy"} - Not available + %span{"ng-show" => "product.group_buy && (product.variants.length == 0)"} + %input{type: :number, + min: 0, + max: "{{product.on_demand && 9999 || product.count_on_hand }}", + name: "variant_attributes[{{product.master.id}}][max_quantity]"} %td.price - %small from + %small{"ng-show" => "(product.variants.length > 0)"} from ${{ product.price }} - %tr{"ng-repeat" => "variant in product.variants"} - %td{colspan: 3} + %tr{"ng-repeat" => "variant in product.variants", "ng-show" => "product.show_variants"} + %td{colspan: 2} %td {{variant.options_text}} %td - %input{type: :number, value: 0, min: 0, name: "variants[{{variant.id}}]"} + %input{type: :number, + value: 0, + min: 0, + max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", + name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} %td.group_buy %span{"ng-show" => "product.group_buy"} - Available - %span{"ng-hide" => "product.group_buy"} - Not available + %input{type: :number, + min: 0, + max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", + name: "variant_attributes[{{variant.id}}][max_quantity]"} %td.price - %small from ${{variant.price }} + %small ${{variant.price }} %input.button.right{type: :submit, value: "Check Out"} -#%pre {{ data.products | json }} diff --git a/app/views/shop/products.rabl b/app/views/shop/products.rabl index 7d245447e9..265c02a08b 100644 --- a/app/views/shop/products.rabl +++ b/app/views/shop/products.rabl @@ -1,20 +1,25 @@ collection @products -attributes :id, :name, :description, :price, :permalink +attributes :id, :name, :price, :permalink, :count_on_hand, :on_demand, :group_buy +node do |product| + {description: strip_tags(product.description)} +end -child :supplier do +child :supplier => :supplier do attributes :id, :name, :description end + child :master => :master do - attributes :id, :is_master, :count_on_hand, :options_text - child :images => :images do - attributes :id, :alt - node do |img| - {:small_url => img.attachment.url(:small, false)} - end - end -end -child :variants => :variants do |variant| - attributes :id, :is_master, :count_on_hand, :options_text + attributes :id, :is_master, :count_on_hand, :options_text, :count_on_hand, :on_demand + child :images => :images do + attributes :id, :alt + node do |img| + {:small_url => img.attachment.url(:small, false)} + end + end +end + +child :variants => :variants do |variant| + attributes :id, :is_master, :price, :count_on_hand, :options_text, :count_on_hand, :on_demand, :group_buy child :images => :images do attributes :id, :alt node do |img| diff --git a/app/views/shop/show.html.haml b/app/views/shop/show.html.haml index a4d54423b6..93d50ab1b2 100644 --- a/app/views/shop/show.html.haml +++ b/app/views/shop/show.html.haml @@ -1,23 +1,26 @@ %shop{"ng-app" => "Shop"} %navigation %distributor.details.row - %img.left{src: ""} - %h4 - = @distributor.name - %location= @distributor.address.city - %small - %a{href: "/"} Change location + #distributor_title + %img.left{src: @distributor.logo.url(:thumb)} + %h4 + = @distributor.name + %location= @distributor.address.city + %small + %a{href: "/"} Change location + = render partial: "shop/order_cycles" -#%description - + %tabs .row .section-container.auto{"data-section" => "", "data-options" => "one_up: false"} - %section + %section#about{class: current_order_cycle ? nil : "active" } %p.title.avenir{"data-section-title" => ""} %a{href: "#about"} About Us .content{"data-section-content" => ""} + %img.about.right{src: @distributor.promo_image.url(:large)} %p= @distributor.long_description.andand.html_safe %section @@ -40,7 +43,6 @@ .content{"data-section-content" => ""} %p Contact - %products.row = render partial: "shop/products" #footer diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index 970bc47961..e2ae2dcee4 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -121,8 +121,8 @@ %td{ 'ng-show' => 'columns.price.visible' } %input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text' } %td{ 'ng-show' => 'columns.on_hand.visible' } - %span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => 'hasVariants(product)' } - %input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-show' => '!hasVariants(product)', :type => 'number' } + %span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' } + %input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-hide' => 'hasVariants(product) || product.on_demand', :type => 'number' } %td{ 'ng-show' => 'columns.available_on.visible' } %input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ofn-track-product' => 'available_on', 'datetimepicker' => 'product.available_on', type: "text" } %td.actions @@ -142,7 +142,8 @@ %td{ 'ng-show' => 'columns.price.visible' } %input{ 'ng-model' => 'variant.price', 'ofn-decimal' => :true, :name => 'variant_price', 'ofn-track-variant' => 'price', :type => 'text' } %td{ 'ng-show' => 'columns.on_hand.visible' } - %input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ofn-track-variant' => 'on_hand', :type => 'number' } + %input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ng-hide' => 'variant.on_demand', 'ofn-track-variant' => 'on_hand', :type => 'number' } + %span{ 'ng-bind' => 'variant.on_hand', :name => 'variant_on_hand', 'ng-show' => 'variant.on_demand' } %td{ 'ng-show' => 'columns.available_on.visible' } %td.actions %a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text" } diff --git a/app/views/spree/api/products/bulk_show.v1.rabl b/app/views/spree/api/products/bulk_show.v1.rabl index 534dd87846..1f07d3d91f 100644 --- a/app/views/spree/api/products/bulk_show.v1.rabl +++ b/app/views/spree/api/products/bulk_show.v1.rabl @@ -1,5 +1,8 @@ object @product -attributes :id, :name, :price, :on_hand, :variant_unit, :variant_unit_scale, :variant_unit_name +attributes :id, :name, :price, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand + +# Infinity is not a valid JSON object, but Rails encodes it anyway +node( :on_hand ) { |p| p.on_hand.to_f.finite? ? p.on_hand : "On demand" } node( :available_on ) { |p| p.available_on.blank? ? "" : p.available_on.strftime("%F %T") } node( :permalink_live ) { |p| p.permalink } @@ -9,4 +12,3 @@ end node( :variants ) do |p| partial 'spree/api/variants/bulk_index', :object => p.variants.order('id ASC') end - diff --git a/app/views/spree/api/variants/bulk_show.v1.rabl b/app/views/spree/api/variants/bulk_show.v1.rabl index fa487068fa..ee069c600c 100644 --- a/app/views/spree/api/variants/bulk_show.v1.rabl +++ b/app/views/spree/api/variants/bulk_show.v1.rabl @@ -1,3 +1,6 @@ object @variant -attributes :id, :options_text, :price, :unit_value, :unit_description, :on_hand \ No newline at end of file +attributes :id, :options_text, :price, :unit_value, :unit_description, :on_demand + +# Infinity is not a valid JSON object, but Rails encodes it anyway +node( :on_hand ) { |p| p.on_hand.to_f.finite? ? p.on_hand : "On demand" } diff --git a/config/environments/production.rb b/config/environments/production.rb index 2e08f8e029..a780f486fb 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -45,6 +45,7 @@ Openfoodnetwork::Application.configure do # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) # config.assets.precompile += %w( search.js ) + require 'uglifier' config.assets.js_compressor = Uglifier.new(mangle: false) # Disable delivery errors, bad email addresses will be ignored diff --git a/config/environments/staging.rb b/config/environments/staging.rb index bee008922f..ee6fa16e8a 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -45,6 +45,7 @@ Openfoodnetwork::Application.configure do # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) # config.assets.precompile += %w( search.js ) + require 'uglifier' config.assets.js_compressor = Uglifier.new(mangle: false) # Disable delivery errors, bad email addresses will be ignored diff --git a/db/migrate/20140116030500_add_attachment_logo_to_enterprise.rb b/db/migrate/20140116030500_add_attachment_logo_to_enterprise.rb new file mode 100644 index 0000000000..0f3c6e114f --- /dev/null +++ b/db/migrate/20140116030500_add_attachment_logo_to_enterprise.rb @@ -0,0 +1,15 @@ +class AddAttachmentLogoToEnterprise < ActiveRecord::Migration + def self.up + add_column :enterprises, :logo_file_name, :string + add_column :enterprises, :logo_content_type, :string + add_column :enterprises, :logo_file_size, :integer + add_column :enterprises, :logo_updated_at, :datetime + end + + def self.down + remove_column :enterprises, :logo_file_name + remove_column :enterprises, :logo_content_type + remove_column :enterprises, :logo_file_size + remove_column :enterprises, :logo_updated_at + end +end diff --git a/db/migrate/20140121050239_add_attachment_promo_image_to_enterprise.rb b/db/migrate/20140121050239_add_attachment_promo_image_to_enterprise.rb new file mode 100644 index 0000000000..d875f111b2 --- /dev/null +++ b/db/migrate/20140121050239_add_attachment_promo_image_to_enterprise.rb @@ -0,0 +1,15 @@ +class AddAttachmentPromoImageToEnterprise < ActiveRecord::Migration + def self.up + add_column :enterprises, :promo_image_file_name, :string + add_column :enterprises, :promo_image_content_type, :string + add_column :enterprises, :promo_image_file_size, :integer + add_column :enterprises, :promo_image_updated_at, :datetime + end + + def self.down + remove_column :enterprises, :promo_image_file_name + remove_column :enterprises, :promo_image_content_type + remove_column :enterprises, :promo_image_file_size + remove_column :enterprises, :promo_image_updated_at + end +end diff --git a/db/schema.rb b/db/schema.rb index 6b0e614cf4..d430638311 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140110040238) do +ActiveRecord::Schema.define(:version => 20140121050239) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -202,9 +202,17 @@ ActiveRecord::Schema.define(:version => 20140110040238) do t.integer "address_id" t.string "pickup_times" t.string "next_collection_at" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.text "distributor_info" + t.string "logo_file_name" + t.string "logo_content_type" + t.integer "logo_file_size" + t.datetime "logo_updated_at" + t.string "promo_image_file_name" + t.string "promo_image_content_type" + t.integer "promo_image_file_size" + t.datetime "promo_image_updated_at" end create_table "exchange_fees", :force => true do |t| @@ -481,9 +489,9 @@ ActiveRecord::Schema.define(:version => 20140110040238) do t.string "email" t.text "special_instructions" t.integer "distributor_id" - t.integer "order_cycle_id" t.string "currency" t.string "last_ip_address" + t.integer "order_cycle_id" t.integer "cart_id" end diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 71f776163a..03e544ca19 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -54,10 +54,8 @@ describe ShopController do spree_get :order_cycle response.body.should have_content oc1.id end - end - it "should not allow the user to select an invalid order cycle" do oc1 = create(:order_cycle, distributors: [d]) oc2 = create(:order_cycle, distributors: [d]) @@ -110,13 +108,27 @@ describe ShopController do context "RABL tests" do render_views - it "only returns products for the current order cycle" do + before do controller.stub(:current_order_cycle).and_return order_cycle + end + it "only returns products for the current order cycle" do xhr :get, :products response.body.should have_content product.name end - end + it "doesn't return products not in stock" do + product.update_attribute(:on_demand, false) + product.master.update_attribute(:count_on_hand, 0) + xhr :get, :products + response.body.should_not have_content product.name + end + it "strips html from description" do + product.update_attribute(:description, "turtles frogs") + xhr :get, :products + response.body.should have_content "frogs" + response.body.should_not have_content " 'Thursday, 22nd Feb, 6 - 9 PM' click_button 'Create' - flash_message.should == 'Enterprise "Eaterprises" has been successfully created!' end @@ -224,5 +223,12 @@ feature %q{ supplier2.reload.next_collection_at.should be_nil distributor2.reload.next_collection_at.should be_nil end + + scenario "Editing images for an enterprise" do + click_link 'Enterprises' + first(".edit").click + page.should have_content "Logo" + page.should have_content "Promo" + end end end diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 05ec22ef41..51c9f83e07 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -144,8 +144,8 @@ feature %q{ table.sort.should == [ ["Supplier", "Producer Suburb", "Product", "Product Properties", "Variant Value", "Price", "Group Buy Unit Quantity", "Amount"], - [product_1.supplier.name, product_1.supplier.address.city, "Product Name", product_1.properties.join(", "), "Size: Test", "100.0", product_1.group_buy_unit_size.to_s, ""], - [product_1.supplier.name, product_1.supplier.address.city, "Product Name", product_1.properties.join(", "), "Size: S", "80.0", product_1.group_buy_unit_size.to_s, ""], + [product_1.supplier.name, product_1.supplier.address.city, "Product Name", product_1.properties.join(", "), "Test", "100.0", product_1.group_buy_unit_size.to_s, ""], + [product_1.supplier.name, product_1.supplier.address.city, "Product Name", product_1.properties.join(", "), "S", "80.0", product_1.group_buy_unit_size.to_s, ""], [product_2.supplier.name, product_1.supplier.address.city, "Product 2", product_1.properties.join(", "), "", "99.0", product_1.group_buy_unit_size.to_s, ""] ].sort end diff --git a/spec/features/consumer/shopping_spec.rb b/spec/features/consumer/shopping_spec.rb index bbf127f1cb..a840e2f92b 100644 --- a/spec/features/consumer/shopping_spec.rb +++ b/spec/features/consumer/shopping_spec.rb @@ -17,6 +17,12 @@ feature "As a consumer I want to shop with a distributor", js: true do page.should have_text distributor.name end + it "shows distributor images" do + visit shop_path + first("distributor img")['src'].should == distributor.logo.url(:thumb) + first("#about img")['src'].should == distributor.promo_image.url(:large) + end + describe "With products in order cycles" do let(:supplier) { create(:supplier_enterprise) } let(:product) { create(:product, supplier: supplier) } @@ -32,37 +38,48 @@ feature "As a consumer I want to shop with a distributor", js: true do click_link "Our Producers" page.should have_content supplier.name end - end - describe "selecting an order cycle" do it "selects an order cycle if only one is open" do # create order cycle oc1 = create(:simple_order_cycle, distributors: [distributor]) exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) exchange.update_attribute :pickup_time, "turtles" - visit shop_path page.should have_selector "option[selected]", text: 'turtles' end describe "with multiple order cycles" do - let(:oc1) {create(:simple_order_cycle, distributors: [distributor])} - let(:oc2) {create(:simple_order_cycle, distributors: [distributor])} + let(:oc1) {create(:simple_order_cycle, distributors: [distributor], orders_close_at: 2.days.from_now)} + let(:oc2) {create(:simple_order_cycle, distributors: [distributor], orders_close_at: 3.days.from_now)} before do exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) exchange.update_attribute :pickup_time, "frogs" exchange = Exchange.find(oc2.exchanges.to_enterprises(distributor).outgoing.first.id) exchange.update_attribute :pickup_time, "turtles" + visit shop_path end it "shows a select with all order cycles" do - visit shop_path page.should have_selector "option", text: 'frogs' page.should have_selector "option", text: 'turtles' end + it "shows the About Us by default if no order cycle is selected" do + page.should have_content "Hello, world!" + end + + it "doesn't show the table before an order cycle is selected" do + page.should_not have_selector("input.button.right", visible: true) + end + + pending "shows the table after an order cycle is selected" do + select "frogs", :from => "order_cycle_id" + save_and_open_page + #page.should have_selector("input.button.right", visible: true) + end + describe "with products in our order cycle" do let(:product) { create(:simple_product) } before do @@ -75,7 +92,7 @@ feature "As a consumer I want to shop with a distributor", js: true do select "frogs", :from => "order_cycle_id" Spree::Order.last.order_cycle.should == nil page.should have_selector "products" - page.should have_content "Orders close #{oc1.orders_close_at.strftime('%A %m')}" + page.should have_content "Orders close 2 days from now" Spree::Order.last.order_cycle.should == oc1 end @@ -89,8 +106,126 @@ feature "As a consumer I want to shop with a distributor", js: true do end it "updates the orders close note when order cycle is changed" do - select "frogs", :from => "order_cycle_id" - page.should have_content "Orders close #{oc1.orders_close_at.strftime('%A %m')}" + oc1.stub(:orders_close_at).and_return 3.days.from_now + select "turtles", :from => "order_cycle_id" + page.should have_content "Orders close 3 days from now" + end + end + end + + describe "After selecting an order cycle with products visible" do + let(:oc) { create(:simple_order_cycle, distributors: [distributor]) } + let(:product) { create(:simple_product) } + let(:variant) { create(:variant, product: product) } + + before do + exchange = Exchange.find(oc.exchanges.to_enterprises(distributor).outgoing.first.id) + exchange.update_attribute :pickup_time, "frogs" + exchange.variants << product.master + exchange.variants << variant + visit shop_path + select "frogs", :from => "order_cycle_id" + exchange + end + + it "should not show quantity field for product with variants" do + page.should_not have_selector("#variants_#{product.master.id}", visible: true) + end + + it "collapses variants by default" do + page.should_not have_text variant.options_text + end + + it "expands variants" do + find(".expand").trigger "click" + page.should have_text variant.options_text + find(".collapse").trigger "click" + page.should_not have_text variant.options_text + end + it "allows the user to expand variants" + end + + describe "Filtering on hand and on demand products" do + let(:oc) { create(:simple_order_cycle, distributors: [distributor]) } + let(:p1) { create(:simple_product, on_demand: false) } + let(:p2) { create(:simple_product, on_demand: true) } + let(:p3) { create(:simple_product, on_demand: false) } + let(:p4) { create(:simple_product, on_demand: false) } + let(:v1) { create(:variant, product: p4) } + + before do + p1.master.count_on_hand = 1 + p2.master.count_on_hand = 0 + p1.master.update_attribute(:count_on_hand, 1) + p2.master.update_attribute(:count_on_hand, 0) + p3.master.update_attribute(:count_on_hand, 0) + v1.update_attribute(:count_on_hand, 1) + exchange = Exchange.find(oc.exchanges.to_enterprises(distributor).outgoing.first.id) + exchange.update_attribute :pickup_time, "frogs" + exchange.variants << p1.master + exchange.variants << p2.master + exchange.variants << p3.master + exchange.variants << v1 + visit shop_path + select "frogs", :from => "order_cycle_id" + exchange + end + + it "shows on hand products" do + page.should have_content p1.name + page.should have_content p4.name + end + it "shows on demand products" do + page.should have_content p2.name + end + it "does not show products that are neither on hand or on demand" do + page.should_not have_content p3.name + end + end + + describe "group buy products" do + let(:oc) { create(:simple_order_cycle, distributors: [distributor]) } + let(:product) { create(:simple_product, group_buy: true) } + + describe "without variants" do + before do + build_and_select_order_cycle + end + + it "should show group buy input" do + page.should have_field "variant_attributes[#{product.master.id}][max_quantity]", :visible => true + 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 + first("form.custom > input.button.right").click + page.should have_content product.name + li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last + li.max_quantity.should == 9 + li.quantity.should == 5 + end + end + + describe "with variants on the product" do + let(:variant) { create(:variant, product: product) } + before do + build_and_select_order_cycle_with_variants + find(".expand").trigger "click" + end + + it "should show group buy input" do + page.should have_field "variant_attributes[#{variant.id}][max_quantity]", :visible => true + end + + it "should save group buy data to ze cart" do + fill_in "variants[#{variant.id}]", with: 6 + fill_in "variant_attributes[#{variant.id}][max_quantity]", with: 7 + first("form.custom > input.button.right").click + page.should have_content product.name + li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last + li.max_quantity.should == 7 + li.quantity.should == 6 end end end @@ -100,14 +235,10 @@ feature "As a consumer I want to shop with a distributor", js: true do let(:product) { create(:simple_product) } let(:variant) { create(:variant, product: product) } before do - exchange = Exchange.find(oc.exchanges.to_enterprises(distributor).outgoing.first.id) - exchange.update_attribute :pickup_time, "frogs" - exchange.variants << product.master - exchange.variants << variant - visit shop_path - select "frogs", :from => "order_cycle_id" + build_and_select_order_cycle_with_variants end it "should let us add products to our cart" do + find(".expand").trigger "click" fill_in "variants[#{variant.id}]", with: "1" first("form.custom > input.button.right").click current_path.should == "/cart" @@ -134,3 +265,23 @@ feature "As a consumer I want to shop with a distributor", js: true do end end end + +def build_and_select_order_cycle + exchange = Exchange.find(oc.exchanges.to_enterprises(distributor).outgoing.first.id) + exchange.update_attribute :pickup_time, "frogs" + exchange.variants << product.master + visit shop_path + select "frogs", :from => "order_cycle_id" + exchange +end + + +def build_and_select_order_cycle_with_variants + exchange = Exchange.find(oc.exchanges.to_enterprises(distributor).outgoing.first.id) + exchange.update_attribute :pickup_time, "frogs" + exchange.variants << product.master + exchange.variants << variant + visit shop_path + select "frogs", :from => "order_cycle_id" + exchange +end diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 72096f6fb1..ef8a7c3cb2 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -466,6 +466,35 @@ describe "AdminProductEditCtrl", -> expect(scope.variantUnitValue(product, variant)).toEqual null + describe "updating the product on hand count", -> + it "updates when product is not available on demand", -> + spyOn(scope, "onHand").andReturn 123 + product = {on_demand: false} + scope.updateOnHand(product) + expect(product.on_hand).toEqual 123 + + it "updates when product's variants are not available on demand", -> + spyOn(scope, "onHand").andReturn 123 + product = {on_demand: false, variants: [{on_demand: false}]} + scope.updateOnHand(product) + expect(product.on_hand).toEqual 123 + + it "does nothing when the product is available on demand", -> + product = {on_demand: true} + scope.updateOnHand(product) + expect(product.on_hand).toBeUndefined() + + it "does nothing when one of the variants is available on demand", -> + product = + on_demand: false + variants: [ + {on_demand: false, on_hand: 10} + {on_demand: true, on_hand: Infinity} + ] + scope.updateOnHand(product) + expect(product.on_hand).toBeUndefined() + + describe "getting on_hand counts when products have variants", -> p1 = undefined p2 = undefined @@ -526,6 +555,24 @@ describe "AdminProductEditCtrl", -> expect(scope.onHand(not_variants: [])).toEqual "error" + describe "determining whether a product has variants that are available on demand", -> + it "returns true when at least one variant does", -> + product = + variants: [ + {on_demand: false} + {on_demand: true} + ] + expect(scope.hasOnDemandVariants(product)).toBe(true) + + it "returns false otherwise", -> + product = + variants: [ + {on_demand: false} + {on_demand: false} + ] + expect(scope.hasOnDemandVariants(product)).toBe(false) + + describe "submitting products to be updated", -> describe "packing products", -> it "extracts variant_unit_with_scale into variant_unit and variant_unit_scale", -> diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 2b7a9b8c69..6c7dfb9091 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -411,5 +411,13 @@ module Spree Spree::Product.all_variant_unit_option_types.sort.should == [ot1, ot2, ot3].sort end end + + describe "Stock filtering" do + it "considers products that are on_demand as being in stock" do + product = create(:simple_product, on_demand: true) + product.master.update_attribute(:count_on_hand, 0) + product.has_stock?.should == true + end + end end end diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index f64db7c6d3..2075912afc 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -182,6 +182,14 @@ module Spree v.send(:option_value_value_unit).should == [123.45, 'g'] end + it "returns a value of 1 when unit value equals the scale" do + p = double(:product, variant_unit: 'weight', variant_unit_scale: 1000.0) + v.stub(:product) { p } + v.stub(:unit_value) { 1000.0 } + + v.send(:option_value_value_unit).should == [1, 'kg'] + end + it "generates values for all weight scales" do [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']].each do |scale, unit| p = double(:product, variant_unit: 'weight', variant_unit_scale: scale)