diff --git a/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee new file mode 100644 index 0000000000..8fd47c49f8 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee @@ -0,0 +1,3 @@ +Darkswarm.controller "GroupsCtrl", ($scope, Groups) -> + $scope.Groups = Groups + $scope.order = 'position' diff --git a/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee new file mode 100644 index 0000000000..4ec316e65f --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee @@ -0,0 +1,9 @@ +Darkswarm.controller "ProductNodeCtrl", ($scope) -> + $scope.price = -> + if $scope.product.variants.length > 0 + prices = (v.price for v in $scope.product.variants) + Math.min.apply(null, prices) + else + $scope.product.price + + $scope.hasVariants = $scope.product.variants.length > 0 diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index 9ba11baa5b..09df18ca38 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -1,8 +1,8 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Product, OrderCycle) -> $scope.data = Product.data $scope.limit = 3 + $scope.ordering = {order: "name"} $scope.order_cycle = OrderCycle.order_cycle - Product.update() $scope.incrementLimit = -> if $scope.limit < $scope.data.products.length @@ -14,8 +14,3 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Product, OrderCycle) - e.preventDefault() $scope.productPrice = (product) -> - if product.variants.length > 0 - prices = (v.price for v in product.variants) - Math.min.apply(null, prices) - else - product.price diff --git a/app/assets/javascripts/darkswarm/directives/modal.js.coffee b/app/assets/javascripts/darkswarm/directives/modal.js.coffee index 8c34ec18a8..73217b3812 100644 --- a/app/assets/javascripts/darkswarm/directives/modal.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/modal.js.coffee @@ -2,13 +2,14 @@ Darkswarm.directive "ofnModal", ($modal)-> restrict: 'E' replace: true transclude: true + scope: {} template: "{{title}}" link: (scope, elem, attrs, ctrl, transclude)-> scope.title = attrs.title + contents = null + transclude scope, (clone)-> + contents = clone - scope.cancel = -> - scope.modalInstance.dismiss("cancel") - - elem.on "click", -> - scope.modalInstance = $modal.open(controller: ctrl, template: transclude()) + elem.on "click", => + scope.modalInstance = $modal.open(controller: ctrl, template: contents) diff --git a/app/assets/javascripts/darkswarm/services/groups.js.coffee b/app/assets/javascripts/darkswarm/services/groups.js.coffee new file mode 100644 index 0000000000..e5e50615e8 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/groups.js.coffee @@ -0,0 +1,4 @@ +Darkswarm.factory 'Groups', (groups) -> + new class Groups + constructor: -> + @groups = groups diff --git a/app/assets/javascripts/darkswarm/services/product.js.coffee b/app/assets/javascripts/darkswarm/services/product.js.coffee index 89778b659c..ca31d5e9c0 100644 --- a/app/assets/javascripts/darkswarm/services/product.js.coffee +++ b/app/assets/javascripts/darkswarm/services/product.js.coffee @@ -1,12 +1,12 @@ Darkswarm.factory 'Product', ($resource) -> new class Product - data: { + constructor: -> + @update() + data: products: null loading: true - } + update: -> @data.products = $resource("/shop/products").query => @data.loading = false @data - all: -> - @data.products || @update() diff --git a/app/assets/stylesheets/darkswarm/mixins.sass b/app/assets/stylesheets/darkswarm/mixins.sass index 1f8086d9f2..fde601f2d9 100644 --- a/app/assets/stylesheets/darkswarm/mixins.sass +++ b/app/assets/stylesheets/darkswarm/mixins.sass @@ -12,7 +12,7 @@ border: 1px solid #999 font-size: 18px @extend .avenir - padding: 22px 18px + padding: 0.75em 1em height: auto margin-bottom: 1em diff --git a/app/assets/stylesheets/darkswarm/product_table.css.sass b/app/assets/stylesheets/darkswarm/product_table.css.sass new file mode 100644 index 0000000000..9b0a7b4598 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/product_table.css.sass @@ -0,0 +1,4 @@ +.product_table + .row + border: 1px solid black + padding: 8px inherit diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 2107542594..0c2972367a 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -1,8 +1,7 @@ @import mixins @import variables +@import branding -product - display: block .darkswarm #search @@ -77,68 +76,58 @@ product products display: block - padding-top: 2.3em + padding-top: 2.3em @media all and (max-width: 768px) padding-top: 1em input.button.right float: left - table - table-layout: fixed - width: 100% - border-collapse: collapse - border: none - th - line-height: 50px - &.name - width: 330px - //&.notes - //width: 140px - &.variant - width: 180px - &.quantity, &.bulk, &.price - width: 90px - .notes - max-width: 300px - td, th + + + product:hover, product:focus, product:active + border-color: $clr-brick + @include box-shadow(0 0 3px 0 $clr-brick-bright) + + .row.variants + border-top: 1px solid $clr-brick-light + background: $clr-brick-ultra-light + + product + @include csstrans + border: 1px solid #989898 + display: block + margin-bottom: 1em !important + + input + margin: 0 + width: 8em + + .columns + padding-top: 1em + padding-bottom: 1em + + .row.summary, .row.variants + @include csstrans + margin-left: 0 + margin-right: 0 + background: #f7f7f7 + border-top: 1px solid #dfdfdf + + .row.summary + @include csstrans background: #fff - > span - min-width: 50px - display: block - tbody - border: 1px solid #cccccc - border-left: 0px - border-right: 0px - td - padding: 20px 0px - &.name - img - float: left - margin-right: 30px - @media all and (max-width: 768px) - margin-right: 1em - div - min-width: 150px - tr.product-description - display: none + + .summary-header + &, & * + @include avenir + color: $clr-brick + + .summary-price + &, & * + @include avenir + + + - // Responsive - @media all and (max-width: 768px) - td.notes, th.notes - display: none - img - width: 20px - height: auto - tr.product-description - display: table-row - td:empty - display: none - - input[type=number] - width: 60px - margin: 0px - display: block - float: right - padding-top: 14px diff --git a/app/assets/stylesheets/groups.css.scss b/app/assets/stylesheets/groups.css.scss new file mode 100644 index 0000000000..c2a5f9013b --- /dev/null +++ b/app/assets/stylesheets/groups.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the groups controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/admin/enterprise_groups_controller.rb b/app/controllers/admin/enterprise_groups_controller.rb index 466bde2cd5..cc2f3ed1db 100644 --- a/app/controllers/admin/enterprise_groups_controller.rb +++ b/app/controllers/admin/enterprise_groups_controller.rb @@ -15,12 +15,10 @@ module Admin redirect_to main_app.admin_enterprise_groups_path end - private def collection EnterpriseGroup.by_position end - end end diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 2bedcf5b6b..3879f16741 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -26,7 +26,11 @@ class CheckoutController < Spree::CheckoutController if @order.next state_callback(:after) else - flash[:error] = @order.errors.full_messages.to_sentence + unless @order.errors.empty? + flash[:error] = @order.errors.full_messages.to_sentence + else + flash[:error] = t(:payment_processing_failed) + end update_failed return end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb new file mode 100644 index 0000000000..84290f20a3 --- /dev/null +++ b/app/controllers/groups_controller.rb @@ -0,0 +1,7 @@ +class GroupsController < BaseController + layout 'darkswarm' + + def index + @groups = EnterpriseGroup.on_front_page.by_position + end +end diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index 8e876513fa..df7076345f 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -11,4 +11,4 @@ Spree::Admin::BaseController.class_eval do authorize! :admin, record authorize! action, record end -end \ No newline at end of file +end diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index dd53294f6f..5c96901c1d 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -4,4 +4,17 @@ Spree::Admin::OverviewController.class_eval do @product_count = Spree::Product.active.managed_by(spree_current_user).count @order_cycle_count = OrderCycle.active.managed_by(spree_current_user).count end -end \ No newline at end of file + + # This is in Spree::Core::ControllerHelpers::Auth + # But you can't easily reopen modules in Ruby + def unauthorized + if try_spree_current_user + flash[:error] = t(:authorization_failure) + redirect_to '/unauthorized' + else + store_location + url = respond_to?(:spree_login_path) ? spree_login_path : root_path + redirect_to url + end + end +end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb new file mode 100644 index 0000000000..c091b2fc82 --- /dev/null +++ b/app/helpers/groups_helper.rb @@ -0,0 +1,2 @@ +module GroupsHelper +end diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 41d2a997cc..6c163b087a 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -4,6 +4,17 @@ class EnterpriseGroup < ActiveRecord::Base has_and_belongs_to_many :enterprises validates :name, presence: true + validates :description, presence: true + + attr_accessible :name, :description, :long_description, :on_front_page, :enterprise_ids + + attr_accessible :promo_image + has_attached_file :promo_image, styles: {medium: "800>400"} + validates_attachment_content_type :promo_image, :content_type => /\Aimage\/.*\Z/ + + attr_accessible :logo + has_attached_file :logo, styles: {medium: "100x100"} + validates_attachment_content_type :logo, :content_type => /\Aimage\/.*\Z/ scope :by_position, order('position ASC') scope :on_front_page, where(on_front_page: true) diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 282a93cfb5..ba7cbc253d 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -118,6 +118,10 @@ Spree::Product.class_eval do order_cycle.variants_distributed_by(distributor).where(product_id: self) end + def primary_taxon + self.taxons.order.first + end + # Build a product distribution for each distributor def build_product_distributions_for_user user Enterprise.is_distributor.managed_by(user).each do |distributor| diff --git a/app/views/admin/enterprise_groups/_form.html.haml b/app/views/admin/enterprise_groups/_form.html.haml index b41a68d4a5..ec4934bf19 100644 --- a/app/views/admin/enterprise_groups/_form.html.haml +++ b/app/views/admin/enterprise_groups/_form.html.haml @@ -3,6 +3,16 @@ %br/ = f.text_field :name += f.field_container :description do + = f.label :description + %br/ + = f.text_field :description + += f.field_container :long_description do + = f.label :long_description + %br/ + = f.text_area :long_description + = f.field_container :on_front_page do = f.label :on_front_page, 'On front page?' %br/ @@ -12,3 +22,22 @@ = f.label :enterprise_ids, 'Enterprises' %br/ = f.collection_select :enterprise_ids, Enterprise.all, :id, :name, {}, {class: "select2 fullwidth", multiple: true} + + +.row + .alpha.three.columns + = f.label :logo, class: 'with-tip', 'data-powertip' => 'This is the logo' + .with-tip{'data-powertip' => 'This is the logo'} + %a What's this? + .omega.eight.columns + = image_tag @object.logo.url if @object.logo.present? + = f.file_field :logo + +.row + .alpha.three.columns + = f.label :promo_image, class: 'with-tip', 'data-powertip' => 'This image is displayed at the top of the Group profile' + .with-tip{'data-powertip' => 'This image is displayed at the top of the Group profile'} + %a What's this? + .omega.eight.columns + = image_tag @object.promo_image.url if @object.promo_image.present? + = f.file_field :promo_image diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml new file mode 100644 index 0000000000..552e45fd48 --- /dev/null +++ b/app/views/groups/index.html.haml @@ -0,0 +1,25 @@ +#groups{"ng-controller" => "GroupsCtrl"} + :javascript + angular.module('Darkswarm').value('groups', #{render partial: "json/groups", object: @groups}) + .row + .small-12.columns.text-center + %h1 Groups / Regions + %h3 Check out our food groups below + + %input{type: :text, + "ng-model" => "query", + placeholder: "Search group name", + "ng-debounce" => "150", + "ofn-disable-enter" => true} + + .row.group_table{bindonce: true} + .small.12.columns + .group{"ng-repeat" => "group in Groups.groups | filter:query | orderBy:order"} + %h2 {{ group.name }} + %p {{ group.description }} + %p {{ group.long_description }} + %img{"bo-src" => "group.logo"} + + %ul + %li{"ng-repeat" => "enterprise in group.enterprises"} + %a{"bo-href" => "enterprise.path"} {{ enterprise.name }} diff --git a/app/views/home/_fat.html.haml b/app/views/home/_fat.html.haml index 7d5dfb3b80..9382844265 100644 --- a/app/views/home/_fat.html.haml +++ b/app/views/home/_fat.html.haml @@ -2,7 +2,8 @@ .columns.small-4 %strong Shop for %p.trans-sentence - {{ hub.taxons | printArrayOfObjects }} + %img{"ng-repeat" => "taxon in hub.taxons", "bo-src" => "taxon.icon", + name: "{{taxon.name}}", alt: "{{taxon.name}}"} .columns.small-4 %strong Delivery options %ol @@ -10,8 +11,9 @@ %li.delivery{"bo-if" => "hub.delivery"} Delivery .columns.small-4 %strong Our producers - %p - Go to our shop to see our current producers + %ul + %li{"ng-repeat" => "producer in hub.producers"} + = render partial: "modals/producer" .row.active_table_row.link{"ng-show" => "open()", "ng-if" => "hub.active"} .columns.small-11 diff --git a/app/views/json/_enterprises.rabl b/app/views/json/_enterprises.rabl index 97e040eca9..2f2f82c944 100644 --- a/app/views/json/_enterprises.rabl +++ b/app/views/json/_enterprises.rabl @@ -4,3 +4,11 @@ attributes :name, :id, :description child :address do extends "json/partials/address" end + +node :path do |enterprise| + shop_enterprise_path(enterprise) +end + +node :hash do |enterprise| + enterprise.to_param +end diff --git a/app/views/json/_groups.rabl b/app/views/json/_groups.rabl new file mode 100644 index 0000000000..9475b425c4 --- /dev/null +++ b/app/views/json/_groups.rabl @@ -0,0 +1,10 @@ +collection @groups +attributes :id, :name, :position, :description, :long_description + +child enterprises: :enterprises do + extends 'json/enterprises' +end + +node :logo do |group| + group.logo(:original) +end diff --git a/app/views/json/_hubs.rabl b/app/views/json/_hubs.rabl index a6958840bc..dc589b878b 100644 --- a/app/views/json/_hubs.rabl +++ b/app/views/json/_hubs.rabl @@ -2,11 +2,15 @@ collection Enterprise.visible.is_distributor extends 'json/enterprises' child distributed_taxons: :taxons do - attributes :name, :id + extends "json/taxon" end child suppliers: :producers do - attributes :name, :id + attributes :name, :id, :description, :long_description + + node :promo_image do |producer| + producer.promo_image.url + end end node :pickup do |hub| @@ -17,14 +21,6 @@ node :delivery do |hub| not hub.shipping_methods.where(:require_ship_address => true).empty? end -node :path do |hub| - shop_enterprise_path(hub) -end - -node :hash do |hub| - hub.to_param -end - node :active do |hub| @active_distributors.include?(hub) end diff --git a/app/views/json/_producers.rabl b/app/views/json/_producers.rabl index 717347e5de..95a133a182 100644 --- a/app/views/json/_producers.rabl +++ b/app/views/json/_producers.rabl @@ -2,7 +2,7 @@ collection @producers extends 'json/enterprises' child supplied_taxons: :taxons do - attributes :name, :id + extends 'json/taxon' end child distributors: :distributors do diff --git a/app/views/json/_taxon.rabl b/app/views/json/_taxon.rabl new file mode 100644 index 0000000000..916abeff78 --- /dev/null +++ b/app/views/json/_taxon.rabl @@ -0,0 +1,5 @@ +attributes :name, :id, :permalink + +node :icon do |taxon| + taxon.icon.url +end diff --git a/app/views/modals/_food_hub.html.haml b/app/views/modals/_food_hub.html.haml index e80ffe3745..9b559d26cc 100644 --- a/app/views/modals/_food_hub.html.haml +++ b/app/views/modals/_food_hub.html.haml @@ -2,4 +2,4 @@ %h5 Our food hubs are the point of contact between you and the people who make your food! %p You can search for a convenient hub by location or name. Some hubs have multiple points where you can pick-up your purchases, and some will also provide delivery options. Each food hub is a sales point with independent business operations and logisitics - so variations between hubs are to be expected. %p You can only shop one food hub at a time. -%a.close-reveal-modal{"ng-click" => "cancel()"} × \ No newline at end of file +%a.close-reveal-modal{"ng-click" => "$close()"} × diff --git a/app/views/modals/_learn_more.html.haml b/app/views/modals/_learn_more.html.haml index 870d5fe0bc..4cb0534c6e 100644 --- a/app/views/modals/_learn_more.html.haml +++ b/app/views/modals/_learn_more.html.haml @@ -6,4 +6,4 @@ %h5 Learn more %p If you want to learn more about the Open Food Network, how it works, and get involved, check out: %a.button.neutral-btn.dark{:href => "http://www.openfoodnetwork.org" , :target => "_blank" } Open Food Network -%a.close-reveal-modal{"ng-click" => "cancel()"} × +%a.close-reveal-modal{"ng-click" => "$close()"} × diff --git a/app/views/modals/_producer.html.haml b/app/views/modals/_producer.html.haml new file mode 100644 index 0000000000..691e9378d3 --- /dev/null +++ b/app/views/modals/_producer.html.haml @@ -0,0 +1,13 @@ +%ofn-modal{title: "{{ producer.name }}"} + #producer_modal{bindonce: true} + .row + .small-12.columns + %img{"bo-src" => "producer.promo_image"} + %h3 {{ producer.name }} + + .row + .small-6.columns + %p + {{ producer.description }} + .small-6.columns + Stay in touch with {{ producer.name }} diff --git a/app/views/shared/menu/_large_menu.html.haml b/app/views/shared/menu/_large_menu.html.haml index 9443dfa979..7a72b80f60 100644 --- a/app/views/shared/menu/_large_menu.html.haml +++ b/app/views/shared/menu/_large_menu.html.haml @@ -21,7 +21,7 @@ %span.nav-primary Producers %li.divider %li - %a{href: ""} + %a{href: main_app.groups_path} %span.nav-primary Groups %li.divider - if admin_user? or enterprise_user? diff --git a/app/views/shared/menu/_mobile_menu.html.haml b/app/views/shared/menu/_mobile_menu.html.haml index 7d37d9b2cb..5e9f22c597 100644 --- a/app/views/shared/menu/_mobile_menu.html.haml +++ b/app/views/shared/menu/_mobile_menu.html.haml @@ -36,17 +36,14 @@ %li %a{href: root_path + "#/#hubs"} %span.nav-primary Hubs - %li %a{href: ""} %span.nav-primary Map - %li %a{href: main_app.producers_path} %span.nav-primary Producers - %li - %a{href: ""} + %a{href: main_app.groups_path} %span.nav-primary Groups diff --git a/app/views/shop/_products.html.haml b/app/views/shop/_products.html.haml deleted file mode 100644 index 7ed6606717..0000000000 --- a/app/views/shop/_products.html.haml +++ /dev/null @@ -1,79 +0,0 @@ -%products{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", -"infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} - - = form_for :order, :url => populate_orders_path, html: {:class => "custom"} do - - %input#search.text{"ng-model" => "query", - placeholder: "Search", - "ng-debounce" => "150", - "ng-keypress" => "searchKeypress($event)"} - %input.button.right{type: :submit, value: "Add to Cart"} - - %table - %thead - %th.name Item - %th.notes Notes - %th.variant Unit - %th.quantity QTY - %th.bulk Bulk - %th.price.text-right Price - %tbody{"ng-show" => "data.loading"} - %tr - %td{colspan: 6} - %h3.text-center Loading Products - %tbody{"ng-repeat" => "product in data.products | filter:query | limitTo: limit track by product.id"} - %tr{"class" => "product product-{{ product.id }}"} - - %td.name{bindonce: "product"} - %img{"bo-src" => "product.master.images[0].small_url"} - %div - %h5 - {{ product.name }} - %a{"data-reveal-id" => "producer_details_{{product.supplier.id}}", "data-reveal" => ""} - {{ product.supplier.name }} - - %td.notes{bindonce: ""} {{ product.notes | truncate:80 }} - - %td{bindonce: ""} - %span{"ng-hide" => "product.variants.length > 0"} {{ 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: nil, - min: 0, - "ofn-disable-scroll" => true, - max: "{{product.on_demand && 9999 || product.count_on_hand }}", - name: "variants[{{product.master.id}}]", - id: "variants_{{product.master.id}}", - "ng-model" => "product.quantity"} - - %td.group_buy - %span{"ng-show" => "product.group_buy && (product.variants.length == 0)"} - %input{type: :number, - min: 0, - "ofn-disable-scroll" => true, - max: "{{product.on_demand && 9999 || product.count_on_hand }}", - name: "variant_attributes[{{product.master.id}}][max_quantity]", - "ng-model" => "product.max_quantity"} - - %td.price.text-right{bindonce: ""} - %small{"ng-show" => "(product.variants.length > 0)"} from - {{ productPrice(product) | currency }} - - %tr.product-description{bindonce: ""} - %td{colspan: 2}{{ product.notes | truncate:80 }} - - %tr.variant{"ng-repeat" => "variant in product.variants", "ng-if" => "product.show_variants"} - = render partial: "shop/variant" - - %input.button.right{type: :submit, value: "Add to Cart"} - - diff --git a/app/views/shop/_variant.html.haml b/app/views/shop/_variant.html.haml deleted file mode 100644 index 15d5485090..0000000000 --- a/app/views/shop/_variant.html.haml +++ /dev/null @@ -1,22 +0,0 @@ - -%td -%td.notes -%td{bindonce: ""} {{variant.options_text}} -%td - %input{type: :number, - value: nil, - min: 0, - "ofn-disable-scroll" => true, - max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", - name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}", - "ng-model" => "variant.quantity"} -%td.group_buy - %span{"ng-show" => "product.group_buy"} - %input{type: :number, - min: 0, - "ofn-disable-scroll" => true, - max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", - name: "variant_attributes[{{variant.id}}][max_quantity]", - "ng-model" => "variant.max_quantity"} -%td.price.text-right{bindonce: ""} - {{ variant.price | currency }} diff --git a/app/views/shop/products.rabl b/app/views/shop/products.rabl index c92a1afaa0..c79157dae5 100644 --- a/app/views/shop/products.rabl +++ b/app/views/shop/products.rabl @@ -1,10 +1,6 @@ collection @products attributes :id, :name, :permalink, :count_on_hand, :on_demand, :group_buy -node :show_variants do - true -end - node do |product| { notes: strip_tags(product.notes), @@ -17,6 +13,10 @@ child :supplier => :supplier do attributes :id, :name, :description end +child :primary_taxon => :primary_taxon do + extends 'json/taxon' +end + child :master => :master do attributes :id, :is_master, :count_on_hand, :options_text, :count_on_hand, :on_demand child :images => :images do diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml new file mode 100644 index 0000000000..8c70f76188 --- /dev/null +++ b/app/views/shop/products/_form.html.haml @@ -0,0 +1,29 @@ +%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", +"infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} + + = form_for :order, :url => populate_orders_path, html: {:class => "custom"} do + + .row + .small-6.columns + %input#search.text{"ng-model" => "query", + placeholder: "Search", + "ng-debounce" => "150", + "ofn-disable-enter" => true} + .small-6.columns + %input.button.right{type: :submit, value: "Add to Cart"} + + %div{bindonce: true} + %product{"ng-controller" => "ProductNodeCtrl", + "ng-repeat" => "product in data.products | filter:query | orderBy:ordering.order | limitTo: limit track by product.id"} + %div + = render partial: "shop/products/summary" + + %div{"bo-if" => "hasVariants"} + = render partial: "shop/products/variants" + + .variant.row{"bo-if" => "!hasVariants"} + = render partial: "shop/products/master" + + .row + .small-12.columns + %input.button.right.add_to_cart{type: :submit, value: "Add to Cart"} diff --git a/app/views/shop/products/_master.html.haml b/app/views/shop/products/_master.html.haml new file mode 100644 index 0000000000..7398912bda --- /dev/null +++ b/app/views/shop/products/_master.html.haml @@ -0,0 +1,39 @@ +.small-1.column + %span.bulk{"bo-if" => "product.group_buy"} bulk +   + +.small-4.columns + ({{ product.master.options_text }}) + +-# WITHOUT GROUP BUY +.small-5.columns{"bo-if" => "!product.group_buy"} + %input{type: :number, + min: 0, + "ofn-disable-scroll" => true, + max: "{{product.on_demand && 9999 || product.count_on_hand }}", + name: "variants[{{product.master.id}}]", + id: "variants_{{product.master.id}}", + "ng-model" => "product.quantity"} + +-# WITH GROUP BUY +.small-2.columns{"bo-if" => "product.group_buy"} + %input{type: :number, + min: 0, + "ofn-disable-scroll" => true, + max: "{{product.on_demand && 9999 || product.count_on_hand }}", + name: "variants[{{product.master.id}}]", + id: "variants_{{product.master.id}}", + "ng-model" => "product.quantity"} + (min) + +.small-3.columns{"bo-if" => "product.group_buy"} + %input{type: :number, + min: 0, + "ofn-disable-scroll" => true, + max: "{{product.on_demand && 9999 || product.count_on_hand }}", + name: "variant_attributes[{{product.master.id}}][max_quantity]", + "ng-model" => "product.max_quantity"} + (max) + +.small-2.columns.text-right + {{ product.price | currency }} diff --git a/app/views/shop/products/_modal.html.haml b/app/views/shop/products/_modal.html.haml new file mode 100644 index 0000000000..29a023704f --- /dev/null +++ b/app/views/shop/products/_modal.html.haml @@ -0,0 +1,2 @@ +%ofn-modal{title: "{{product.name}}"} + {{ product.description }} diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml new file mode 100644 index 0000000000..5e777a5dbb --- /dev/null +++ b/app/views/shop/products/_summary.html.haml @@ -0,0 +1,18 @@ +.row.summary + .small-1.column + %img{"bo-src" => "product.master.images[0].small_url"} + + .small-4.columns.summary-header + %img{"bo-src" => "product.primary_taxon.icon", + "ng-click" => "ordering.order = 'primary_taxon.name'", + name: "{{product.primary_taxon.name}}"} + {{ product.name}} + -#= render partial: "shop/products/modal" + + .small-5.columns.summary-header + {{ product.supplier.name }} + + .small-2.columns.summary-price.text-right.price + %span{"ng-if" => "hasVariants"} + %em from + {{ price() | currency }} diff --git a/app/views/shop/products/_variants.html.haml b/app/views/shop/products/_variants.html.haml new file mode 100644 index 0000000000..14ff8eff24 --- /dev/null +++ b/app/views/shop/products/_variants.html.haml @@ -0,0 +1,42 @@ +.row.variants{bindonce: true, + "ng-repeat" => "variant in product.variants"} + + .small-1.column + %span.bulk{"bo-if" => "product.group_buy"} bulk +   + + .small-4.columns + {{ variant.options_text }} + + -# WITHOUT GROUP BUY + .small-5.columns{"bo-if" => "!product.group_buy"} + %input{type: :number, + value: nil, + min: 0, + "ofn-disable-scroll" => true, + max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", + name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}", + "bo-model" => "variant.quantity"} + + -# WITH GROUP BUY + .small-2.columns{"bo-if" => "product.group_buy"} + %input{type: :number, + value: nil, + min: 0, + "ofn-disable-scroll" => true, + max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", + name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}", + "bo-model" => "variant.quantity"} + (min) + + .small-3.columns{"bo-if" => "product.group_buy"} + %input{type: :number, + min: 0, + "ofn-disable-scroll" => true, + max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", + name: "variant_attributes[{{variant.id}}][max_quantity]", + "ng-model" => "variant.max_quantity"} + (max) + + .small-2.columns.text-right.price + {{ variant.price | currency }} diff --git a/app/views/shop/show.html.haml b/app/views/shop/show.html.haml index ecfcc8e4e3..ba9493e46b 100644 --- a/app/views/shop/show.html.haml +++ b/app/views/shop/show.html.haml @@ -13,5 +13,5 @@ = render partial: "shopping_shared/details" - %products.row - = render partial: "shop/products" + .row + = render partial: "shop/products/form" diff --git a/config/routes.rb b/config/routes.rb index e4868d777c..310d1d9e4d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,12 +1,15 @@ Openfoodnetwork::Application.routes.draw do root :to => 'home#index' + get "/#/login", to: "home#index", as: :spree_login + resource :shop, controller: "shop" do get :products post :order_cycle get :order_cycle end + resources :groups resources :producers get '/checkout', :to => 'checkout#edit' , :as => :checkout diff --git a/db/migrate/20140516042552_add_attachment_promo_image_to_enterprise_group.rb b/db/migrate/20140516042552_add_attachment_promo_image_to_enterprise_group.rb new file mode 100644 index 0000000000..bd67cb92f0 --- /dev/null +++ b/db/migrate/20140516042552_add_attachment_promo_image_to_enterprise_group.rb @@ -0,0 +1,15 @@ +class AddAttachmentPromoImageToEnterpriseGroup < ActiveRecord::Migration + def self.up + add_column :enterprise_groups, :promo_image_file_name, :string + add_column :enterprise_groups, :promo_image_content_type, :string + add_column :enterprise_groups, :promo_image_file_size, :integer + add_column :enterprise_groups, :promo_image_updated_at, :datetime + end + + def self.down + remove_column :enterprise_groups, :promo_image_file_name + remove_column :enterprise_groups, :promo_image_content_type + remove_column :enterprise_groups, :promo_image_file_size + remove_column :enterprise_groups, :promo_image_updated_at + end +end diff --git a/db/migrate/20140516044750_add_fields_to_groups.rb b/db/migrate/20140516044750_add_fields_to_groups.rb new file mode 100644 index 0000000000..a50ef1db14 --- /dev/null +++ b/db/migrate/20140516044750_add_fields_to_groups.rb @@ -0,0 +1,6 @@ +class AddFieldsToGroups < ActiveRecord::Migration + def change + add_column :enterprise_groups, :description, :text + add_column :enterprise_groups, :long_description, :text + end +end diff --git a/db/migrate/20140516045323_add_attachment_logo_to_enterprise_group.rb b/db/migrate/20140516045323_add_attachment_logo_to_enterprise_group.rb new file mode 100644 index 0000000000..525bccb63e --- /dev/null +++ b/db/migrate/20140516045323_add_attachment_logo_to_enterprise_group.rb @@ -0,0 +1,15 @@ +class AddAttachmentLogoToEnterpriseGroup < ActiveRecord::Migration + def self.up + add_column :enterprise_groups, :logo_file_name, :string + add_column :enterprise_groups, :logo_content_type, :string + add_column :enterprise_groups, :logo_file_size, :integer + add_column :enterprise_groups, :logo_updated_at, :datetime + end + + def self.down + remove_column :enterprise_groups, :logo_file_name + remove_column :enterprise_groups, :logo_content_type + remove_column :enterprise_groups, :logo_file_size + remove_column :enterprise_groups, :logo_updated_at + end +end diff --git a/db/schema.rb b/db/schema.rb index cacc8fc793..5394fcbd61 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 => 20140514044959) do +ActiveRecord::Schema.define(:version => 20140516045323) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -182,9 +182,19 @@ ActiveRecord::Schema.define(:version => 20140514044959) do add_index "enterprise_fees", ["enterprise_id"], :name => "index_enterprise_fees_on_enterprise_id" create_table "enterprise_groups", :force => true do |t| - t.string "name" - t.boolean "on_front_page" - t.integer "position" + t.string "name" + t.boolean "on_front_page" + t.integer "position" + t.string "promo_image_file_name" + t.string "promo_image_content_type" + t.integer "promo_image_file_size" + t.datetime "promo_image_updated_at" + t.text "description" + t.text "long_description" + t.string "logo_file_name" + t.string "logo_content_type" + t.integer "logo_file_size" + t.datetime "logo_updated_at" end create_table "enterprise_groups_enterprises", :id => false, :force => true do |t| diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb new file mode 100644 index 0000000000..4005d82a2a --- /dev/null +++ b/spec/controllers/groups_controller_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe GroupsController do + it "gets all visible groups" do + EnterpriseGroup.stub_chain :on_front_page, :by_position + EnterpriseGroup.should_receive :on_front_page + get :index + end +end diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 88cde0e6d9..f0857f54bc 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -111,90 +111,6 @@ describe ShopController do response.body.should be_empty end - # TODO: this should be a controller test baby - pending "filtering products" do - let(:distributor) { create(:distributor_enterprise) } - let(:supplier) { create(:supplier_enterprise) } - let(:oc1) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), orders_close_at: 2.days.from_now) } - 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(:p5) { create(:simple_product, on_demand: false) } - let(:p6) { create(:simple_product, on_demand: false) } - let(:p7) { create(:simple_product, on_demand: false) } - let(:v1) { create(:variant, product: p4, unit_value: 2) } - let(:v2) { create(:variant, product: p4, unit_value: 3, on_demand: false) } - let(:v3) { create(:variant, product: p4, unit_value: 4, on_demand: true) } - let(:v4) { create(:variant, product: p5) } - let(:v5) { create(:variant, product: p5) } - let(:v6) { create(:variant, product: p7) } - let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) } - - 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) - p6.master.update_attribute(:count_on_hand, 1) - p6.delete - p7.master.update_attribute(:count_on_hand, 1) - v1.update_attribute(:count_on_hand, 1) - v2.update_attribute(:count_on_hand, 0) - v3.update_attribute(:count_on_hand, 0) - v4.update_attribute(:count_on_hand, 1) - v5.update_attribute(:count_on_hand, 0) - v6.update_attribute(:count_on_hand, 1) - v6.update_attribute(:deleted_at, Time.now) - exchange = Exchange.find(oc1.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 << p6.master - exchange.variants << v1 - exchange.variants << v2 - exchange.variants << v3 - # v4 is in stock but not in distribution - # v5 is out of stock and in the distribution - # Neither should display, nor should their product, p5 - exchange.variants << v5 - exchange.variants << v6 - - controller.stub(:current_order).and_return order - visit shop_path - end - - it "filters products based on availability" do - # It shows on hand products - page.should have_content p1.name - page.should have_content p4.name - - # It shows on demand products - page.should have_content p2.name - - # It does not show products that are neither on hand or on demand - page.should_not have_content p3.name - - # It shows on demand variants - page.should have_content v3.options_text - - # It does not show variants that are neither on hand or on demand - page.should_not have_content v2.options_text - - # It does not show products that have no available variants in this distribution - page.should_not have_content p5.name - - # It does not show deleted products - page.should_not have_content p6.name - - # It does not show deleted variants - page.should_not have_content v6.name - page.should_not have_content p7.name - end - end - context "RABL tests" do render_views before do @@ -211,6 +127,7 @@ describe ShopController do 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 @@ -223,6 +140,14 @@ describe ShopController do xhr :get, :products response.body.should have_content "998.0" end + + it "includes the primary taxon" do + taxon = mock_model(Spree::Taxon, name: "fruitbat") + Spree::Product.any_instance.stub(:primary_taxon).and_return taxon + taxon.stub_chain(:icon, :url).and_return "" + xhr :get, :products + response.body.should have_content "fruitbat" + end end end end diff --git a/spec/factories.rb b/spec/factories.rb index 710ba1c8ba..13c0bd0e8e 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -98,6 +98,7 @@ FactoryGirl.define do factory :enterprise_group, :class => EnterpriseGroup do name 'Enterprise group' + description 'this is a group' on_front_page false end diff --git a/spec/features/admin/enterprise_groups_spec.rb b/spec/features/admin/enterprise_groups_spec.rb index 012179e75a..24e89e9be7 100644 --- a/spec/features/admin/enterprise_groups_spec.rb +++ b/spec/features/admin/enterprise_groups_spec.rb @@ -33,6 +33,7 @@ feature %q{ click_link 'New Enterprise Group' fill_in 'enterprise_group_name', with: 'EGEGEG' + fill_in 'enterprise_group_description', with: 'This is a description' check 'enterprise_group_on_front_page' select e1.name, from: 'enterprise_group_enterprise_ids' select e2.name, from: 'enterprise_group_enterprise_ids' @@ -42,6 +43,7 @@ feature %q{ eg = EnterpriseGroup.last eg.name.should == 'EGEGEG' + eg.description.should == 'This is a description' eg.on_front_page.should be_true eg.enterprises.sort.should == [e1, e2].sort end @@ -62,6 +64,7 @@ feature %q{ fill_in 'enterprise_group_name', with: 'xyzzy' uncheck 'enterprise_group_on_front_page' unselect e1.name, from: 'enterprise_group_enterprise_ids' + select e2.name, from: 'enterprise_group_enterprise_ids' click_button 'Update' @@ -99,7 +102,6 @@ feature %q{ EnterpriseGroup.all.should_not include eg end - context "as an enterprise user" do xit "should show me only enterprises I manage when creating a new enterprise group" end diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 19fcb5a44b..9946be2f5b 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -90,10 +90,6 @@ feature "As a consumer I want to shop with a distributor", js: true do it "should not show quantity field for product with variants" do visit shop_path page.should_not have_selector("#variants_#{product.master.id}", visible: true) - - #it "expands variants" do - find(".collapse").trigger "click" - page.should_not have_text variant1.options_text end it "uses the adjusted price" do @@ -104,15 +100,16 @@ feature "As a consumer I want to shop with a distributor", js: true do visit shop_path # Page should not have product.price (with or without fee) - page.should_not have_selector 'tr.product > td', text: "from $10.00" - page.should_not have_selector 'tr.product > td', text: "from $33.00" + page.should_not have_price "from $10.00" + page.should_not have_price "from $33.00" # Page should have variant prices (with fee) - page.should have_selector 'tr.variant > td.price', text: "$43.00" - page.should have_selector 'tr.variant > td.price', text: "$53.00" + page.should have_price "$43.00" + page.should have_price "$53.00" # Product price should be listed as the lesser of these - page.should have_selector 'tr.product > td', text: "from $43.00" + #page.should have_selector 'tr.product > td', text: "from $43.00" + page.should have_price "from $43.00" end end @@ -131,7 +128,7 @@ feature "As a consumer I want to shop with a distributor", js: true do it "should save group buy data to ze cart" do fill_in "variants[#{product.master.id}]", with: 5 fill_in "variant_attributes[#{product.master.id}][max_quantity]", with: 9 - first("form.custom > input.button.right").click + add_to_cart page.should have_content product.name li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last li.max_quantity.should == 9 @@ -142,7 +139,7 @@ feature "As a consumer I want to shop with a distributor", js: true do 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 fill_in "variant_attributes[#{product.master.id}][max_quantity]", with: 1 - first("form.custom > input.button.right").click + add_to_cart page.should have_content product.name li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last li.max_quantity.should == 5 @@ -161,7 +158,7 @@ feature "As a consumer I want to shop with a distributor", js: true do it "should save group buy data to ze cart" do fill_in "variants[#{variant.id}]", with: 6 fill_in "variant_attributes[#{variant.id}][max_quantity]", with: 7 - first("form.custom > input.button.right").click + add_to_cart page.should have_content product.name li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last li.max_quantity.should == 7 @@ -181,7 +178,7 @@ feature "As a consumer I want to shop with a distributor", js: true do end it "should let us add products to our cart" do fill_in "variants[#{variant.id}]", with: "1" - first("form.custom > input.button.right").click + add_to_cart current_path.should == "/cart" page.should have_content product.name end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb new file mode 100644 index 0000000000..08a4335bae --- /dev/null +++ b/spec/helpers/groups_helper_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +# Specs in this file have access to a helper object that includes +# the GroupsHelper. For example: +# +# describe GroupsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +describe GroupsHelper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/enterprise_group_spec.rb b/spec/models/enterprise_group_spec.rb index 167a8c2f7b..3e38882064 100644 --- a/spec/models/enterprise_group_spec.rb +++ b/spec/models/enterprise_group_spec.rb @@ -11,6 +11,13 @@ describe EnterpriseGroup do e = build(:enterprise_group, name: '') e.should_not be_valid end + + it "requires a description" do + e = build(:enterprise_group, description: '') + end + + it { should have_attached_file :promo_image } + it { should have_attached_file :logo } end describe "relations" do diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 6743e404e9..6d90b70761 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -547,5 +547,15 @@ module Spree end end end + + describe "Taxons" do + let(:taxon1) { create(:taxon) } + let(:taxon2) { create(:taxon) } + let(:product) { create(:simple_product, taxons: [taxon1, taxon2]) } + + it "returns the first taxon as the primary taxon" do + product.primary_taxon.should == taxon1 + end + end end end diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index bdad9e5d49..c87d33149a 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -1,4 +1,12 @@ module ShopWorkflow + def add_to_cart + first("input.add_to_cart").click + end + + def have_price(price) + have_selector ".price", text: price + end + def set_order(order) ApplicationController.any_instance.stub(:session).and_return({order_id: order.id, access_token: order.token}) end