From 8da98ce19cc022c4d67a7150f25a28e5218aad00 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 28 Aug 2014 16:26:30 +1000 Subject: [PATCH 001/681] Adding asterisks to required fields. Addressing BugHerd #454. --- app/views/admin/enterprises/_form.html.haml | 12 ++++++++++-- app/views/admin/enterprises/_required.html.haml | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 app/views/admin/enterprises/_required.html.haml diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 07be41dd6e..b5107e2bc4 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -9,11 +9,13 @@ %fieldset.eleven.columns.alpha.no-border-bottom %legend Primary Details .row - .alpha.eleven.columns + .alpha.twelve.columns .three.columns.alpha = f.label :name .eight.columns.omega = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles" } + .one.columns.omega + = render 'required' .row .alpha.eleven.columns .three.columns.alpha @@ -79,13 +81,15 @@ = f.fields_for :address do |af| - %fieldset.eleven.columns.alpha.no-border-bottom + %fieldset.twelve.columns.alpha.no-border-bottom %legend Address .row .three.columns.alpha = af.label :address1 .eight.columns.omega = af.text_field :address1, { placeholder: "eg. 123 High Street"} + .one.columns.omega + = render 'required' .row .alpha.three.columns = af.label :address2 @@ -100,6 +104,8 @@ = af.text_field :city, { placeholder: "eg. Northcote"} .four.columns.omega = af.text_field :zipcode, { placeholder: "eg. 3070"} + .one.columns.omega + = render 'required' .row .three.columns.alpha = af.label :state_id, 'State' @@ -109,6 +115,8 @@ = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" .four.columns.omega = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" + .one.columns.omega + = render 'required' %fieldset.eleven.columns.alpha.no-border-bottom %legend Contact Details .row diff --git a/app/views/admin/enterprises/_required.html.haml b/app/views/admin/enterprises/_required.html.haml new file mode 100644 index 0000000000..9a0c2ca34b --- /dev/null +++ b/app/views/admin/enterprises/_required.html.haml @@ -0,0 +1,2 @@ +.with-tip{'data-powertip' => 'Required - You need to fill this field to complete the form.'} + %a * From a0d6ec988ea46a1b2fb4d57586c6d47fa5eb57d1 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 28 Aug 2014 16:38:44 +1000 Subject: [PATCH 002/681] make asterisk red --- app/views/admin/enterprises/_required.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/enterprises/_required.html.haml b/app/views/admin/enterprises/_required.html.haml index 9a0c2ca34b..1fc3850257 100644 --- a/app/views/admin/enterprises/_required.html.haml +++ b/app/views/admin/enterprises/_required.html.haml @@ -1,2 +1,2 @@ .with-tip{'data-powertip' => 'Required - You need to fill this field to complete the form.'} - %a * + %a{style: 'color: red'} * From a379f88add80d3c1e8992ef07610490b68b17349 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 28 Aug 2014 16:41:10 +1000 Subject: [PATCH 003/681] making asterisk 10% bigger --- app/views/admin/enterprises/_required.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/enterprises/_required.html.haml b/app/views/admin/enterprises/_required.html.haml index 1fc3850257..1b3574de9f 100644 --- a/app/views/admin/enterprises/_required.html.haml +++ b/app/views/admin/enterprises/_required.html.haml @@ -1,2 +1,2 @@ .with-tip{'data-powertip' => 'Required - You need to fill this field to complete the form.'} - %a{style: 'color: red'} * + %a{style: 'color: red; font-size: 110%'} * From ef639b95357511729c2538a9aac33af3a70c1bfe Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 28 Aug 2014 17:06:26 +1000 Subject: [PATCH 004/681] Adjusting columns for 16 column layout. Removed the 1col spacer column and extended the fieldsets to 12col. --- app/views/admin/enterprises/_form.html.haml | 10 +++++----- app/views/admin/enterprises/_ng_form.html.haml | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index b5107e2bc4..75e1a7be0d 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -6,7 +6,7 @@ .fullwidth_inputs - %fieldset.eleven.columns.alpha.no-border-bottom + %fieldset.twelve.columns.alpha.no-border-bottom %legend Primary Details .row .alpha.twelve.columns @@ -117,7 +117,7 @@ = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" .one.columns.omega = render 'required' - %fieldset.eleven.columns.alpha.no-border-bottom + %fieldset.twelve.columns.alpha.no-border-bottom %legend Contact Details .row .alpha.three.columns @@ -134,7 +134,7 @@ = f.label :phone .omega.eight.columns = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} - %fieldset.eleven.columns.alpha.no-border-bottom + %fieldset.twelve.columns.alpha.no-border-bottom %legend Enterprise Details .row .alpha.three.columns @@ -171,7 +171,7 @@ = f.label :twitter .omega.eight.columns = f.text_field :twitter, { placeholder: "eg. @the_prof" } - %fieldset.eleven.columns.alpha.no-border-bottom + %fieldset.twelve.columns.alpha.no-border-bottom %legend About Us .row .alpha.three.columns @@ -195,7 +195,7 @@ .omega.eight.columns = f.text_area :distributor_info, class: 'rich_text', placeholder: 'Hub only: Explain your distribution offer/s - this is more detailed information that the user can access by clicking on "How does it work?"' / TODO: editor breaks scrolling with arrow keys - %fieldset.eleven.columns.alpha.no-border-bottom + %fieldset.twelve.columns.alpha.no-border-bottom %legend IMAGES .row .alpha.three.columns diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 0ccd963d31..b9e94e1b52 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -3,8 +3,7 @@ = admin_inject_shipping_methods .sixteen.columns.alpha{ ng: { app: 'admin.enterprises', controller: 'enterpriseCtrl' } } - .eleven.columns.alpha + .twelve.columns.alpha = render 'form', f: f - .one.column   .four.columns.omega = render 'sidebar', f: f From 9448e1cbe4b4c4756d98b9b59d2fd8eb00db659b Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Tue, 16 Sep 2014 00:13:43 +1000 Subject: [PATCH 005/681] add figaro env config --- Gemfile | 1 + Gemfile.lock | 4 ++++ ...nfirmation.text.erb => signup_confirmation.text.haml} | 4 ++-- config/application.rb | 4 ++-- config/initializers/secret_token.rb | 8 ++++++-- config/initializers/spree.rb | 9 +++++++-- 6 files changed, 22 insertions(+), 8 deletions(-) rename app/views/spree/user_mailer/{signup_confirmation.text.erb => signup_confirmation.text.haml} (76%) diff --git a/Gemfile b/Gemfile index 456234f554..3f84c28183 100644 --- a/Gemfile +++ b/Gemfile @@ -42,6 +42,7 @@ gem 'gmaps4rails' gem 'spinjs-rails' gem 'rack-ssl', :require => 'rack/ssl' gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg' +gem 'figaro' gem 'foreigner' gem 'immigrant' diff --git a/Gemfile.lock b/Gemfile.lock index 9bb6e501db..94c6ead27a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -243,6 +243,9 @@ GEM railties (>= 3.0.0) ffaker (1.15.0) ffi (1.9.3) + figaro (0.7.0) + bundler (~> 1.0) + rails (>= 3, < 5) fog (1.14.0) builder excon (~> 0.25.0) @@ -520,6 +523,7 @@ DEPENDENCIES debugger-linecache deface! factory_girl_rails + figaro foreigner foundation-icons-sass-rails foundation-rails diff --git a/app/views/spree/user_mailer/signup_confirmation.text.erb b/app/views/spree/user_mailer/signup_confirmation.text.haml similarity index 76% rename from app/views/spree/user_mailer/signup_confirmation.text.erb rename to app/views/spree/user_mailer/signup_confirmation.text.haml index 295b4c9e8f..3a30b6827e 100644 --- a/app/views/spree/user_mailer/signup_confirmation.text.erb +++ b/app/views/spree/user_mailer/signup_confirmation.text.haml @@ -1,6 +1,6 @@ Hello, -Welcome to Australia's Open Food Network! Your login email is <%= @user.email %> +Welcome to #{ENV['DEFAULT_COUNTRY']}'s Open Food Network! Your login email is #{@user.email} You can go online and start shopping through food hubs and local producers you like at http://openfoodnetwork.org.au @@ -9,5 +9,5 @@ We welcome all your questions and feedback; you can use the Send Feedback button Thanks for getting on board and we look forward to introducing you to many more great farmers, food hubs and food! Cheers, -Kirsten Larsen and the OFN Team +#{ENV['CONTACT_STRING']} diff --git a/config/application.rb b/config/application.rb index 1cddfcf3a1..d3bbdbf3fd 100644 --- a/config/application.rb +++ b/config/application.rb @@ -57,11 +57,11 @@ module Openfoodnetwork # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - config.time_zone = 'Melbourne' + config.time_zone = ENV["TIMEZONE"] # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - config.i18n.default_locale = 'en' + config.i18n.default_locale = ENV["LOCALE"] # Setting this to true causes a performance regression in Rails 3.2.17 # When we're on a version with the fix below, we can set it to true diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index fcd0a6dda4..9aac0efd56 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -3,5 +3,9 @@ # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -Openfoodnetwork::Application.config.secret_token = '6d784d49173d0ec820f20cfce151717bd12570e9d261460e9d3c295b90c1fd81e3843eb1bec79d9e6d4a7f04d0fd76170ca0c326ffb0f2da5b7a0b50c7442a4c' +# no regular words or you'll be exposed to dictionary attacks. +Openfoodnetwork::Application.config.secret_token = if Rails.env.development? or Rails.env.test? + ('x' * 30) # Meets basic minimum of 30 chars. +else + ENV["SECRET_TOKEN"] +end diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index bbee30379a..8078dac761 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -11,11 +11,16 @@ require 'spree/product_filters' Spree.config do |config| config.shipping_instructions = true - config.checkout_zone = 'Australia' + config.checkout_zone = ENV["CHECKOUT_ZONE"] config.address_requires_state = true # 12 should be Australia. Hardcoded for CI (Jenkins), where countries are not pre-loaded. - config.default_country_id = 12 + if Rails.env.test? or Rails.env.development? + config.default_country_id = 12 + else + country = Spree::Country.find_by_name(ENV["DEFAULT_COUNTRY"]) + config.default_country_id = country.id if country.present? + end # -- spree_paypal_express # Auto-capture payments. Without this option, payments must be manually captured in the paypal interface. From 15bfe75313dc1bad9260d35070da29c11b8a195b Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Tue, 16 Sep 2014 00:21:10 +1000 Subject: [PATCH 006/681] ifnore figaro --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index b587584d3b..8392b92c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,7 @@ config/initializers/feature_toggle.rb NERD_tree* coverage libpeerconnection.log +tags + +# Ignore application configuration +/config/application.yml From a5680e611e6e8ab4da93b38bbd12b55027d5855b Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Wed, 17 Sep 2014 12:05:28 +1000 Subject: [PATCH 007/681] add example yml --- config/application.yml.example | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config/application.yml.example diff --git a/config/application.yml.example b/config/application.yml.example new file mode 100644 index 0000000000..986e1c9e22 --- /dev/null +++ b/config/application.yml.example @@ -0,0 +1,16 @@ +# Add application configuration variables here, as shown below. +# +# Change this, it has serious security implications. +# Minimum 30 but usually 128 characters. To obtain run 'rake secret', or faster, 'openssl rand -hex 128' +SECRET_TOKEN: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +TIMEZONE: "Melbourne" +# Default country for dropdowns etc. +DEFAULT_COUNTRY: "Australia" +# Locale for translation. +I18N_LOCALE: "en" +# Spree zone. +CHECKOUT_ZONE: "Australia" +# Contact name for emails. +CONTACT_STRING: "Joe Bloggs and the OFN Team" + From 7313aecd1908cd604a8110d7c00f55037edb3625 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 3 Oct 2014 12:19:58 +1000 Subject: [PATCH 008/681] add route checking, not working yet... --- .../enterprises/controllers/enterprise_controller.js.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index c5b38191ba..959708e146 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,8 +1,10 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, Enterprise, PaymentMethods, ShippingMethods) -> + .controller "enterpriseCtrl", ($scope, $rootScope, Enterprise, PaymentMethods, ShippingMethods) -> $scope.Enterprise = Enterprise.enterprise $scope.PaymentMethods = PaymentMethods.paymentMethods $scope.ShippingMethods = ShippingMethods.shippingMethods + $scope.$on "$routeChangeStart", (event, newUrl, oldUrl) -> + event.preventDefault() for payment_method in $scope.PaymentMethods payment_method.selected = payment_method.id in $scope.Enterprise.payment_method_ids @@ -32,4 +34,4 @@ angular.module("admin.enterprises") $scope.ShippingMethods.reduce (count, shipping_method) -> count++ if shipping_method.selected count - , 0 \ No newline at end of file + , 0 From b68754d6345737d596976e6a98df4bd963180c39 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 10 Oct 2014 15:22:21 +1100 Subject: [PATCH 009/681] put payment description in panel --- app/views/checkout/_payment.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index 21dca1542c..2ebc956b59 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -44,6 +44,6 @@ .row{"ng-if" => "order.payment_method_id == #{method.id}"} .small-12.columns = render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } - .small-6.columns - %small {{ Checkout.paymentMethod().description }} - + .small-12.columns.medium-6.columns.large-6.columns + #distributor_address.panel{"ng-show" => "Checkout.paymentMethod().description"} + %span{ style: "white-space: pre-wrap;" }{{ Checkout.paymentMethod().description }} From 01aa8cb761c933fc70e7584efeb72217ae3a9099 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 10 Oct 2014 15:30:53 +1100 Subject: [PATCH 010/681] Use serialiser instead of rabl for current order injection --- app/views/checkout/_form.html.haml | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index 52f6f983cc..bcbcb615fc 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -6,20 +6,14 @@ = inject_available_shipping_methods = inject_available_payment_methods - :javascript - angular.module('Darkswarm').value('order', #{render "checkout/order"}) + = inject_current_order - %div - / %h3.text-center.pad-top - / Checkout from - / = current_distributor.name - - = render partial: "checkout/details", locals: {f: f} - = render partial: "checkout/billing", locals: {f: f} - = render partial: "checkout/shipping", locals: {f: f} - = render partial: "checkout/payment", locals: {f: f} - %p - %button.button.primary{type: :submit, - "ng-disabled" => "checkout.$invalid"} - Place order now - / {{ checkout.$valid }} + = render partial: "checkout/details", locals: {f: f} + = render partial: "checkout/billing", locals: {f: f} + = render partial: "checkout/shipping", locals: {f: f} + = render partial: "checkout/payment", locals: {f: f} + %p + %button.button.primary{type: :submit, + "ng-disabled" => "checkout.$invalid"} + Place order now + / {{ checkout.$valid }} From 3ad7165fd6ade1ea0b88b14c36d49fe82edf1579 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 10 Oct 2014 17:53:28 +1100 Subject: [PATCH 011/681] add checkout subbmission directive --- .../darkswarm/directives/submit_checkout.js.coffee | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/assets/javascripts/darkswarm/directives/submit_checkout.js.coffee diff --git a/app/assets/javascripts/darkswarm/directives/submit_checkout.js.coffee b/app/assets/javascripts/darkswarm/directives/submit_checkout.js.coffee new file mode 100644 index 0000000000..207a76049a --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/submit_checkout.js.coffee @@ -0,0 +1,13 @@ +Darkswarm.directive "submitCheckout", () -> + restrict: "A" + link: (scope, elm, attr)-> + elm.bind 'click', (ev)-> + ev.preventDefault() + + names = ["details", "billing", "shipping", "payment"] + for name of names + if not scope[name].$valid + $scope.show name + # else + # scope.purchase(ev) + From 01c179856a4906deb5e2fda80e3ceaabc77689f9 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 12 Oct 2014 21:19:59 +1100 Subject: [PATCH 012/681] set maxlength for short description field --- app/views/admin/enterprises/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 921807b425..9a3cf1bf39 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -177,7 +177,7 @@ .alpha.three.columns = f.label :description, 'Short Description' .omega.eight.columns - = f.text_field :description, placeholder: 'Tell us about your enterprise in one or two sentences' + = f.text_field :description, maxlength: 255, placeholder: 'Tell us about your enterprise in one or two sentences' .row .alpha.three.columns = f.label :long_description, 'About Us' From 5dd9879b96abd020c82d63bb1a4af8802cafca6f Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 12 Oct 2014 21:57:01 +1100 Subject: [PATCH 013/681] validate description max lenth --- app/models/enterprise.rb | 2 +- spec/models/enterprise_spec.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index dfb7945878..3d1d8ff9bd 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -1,7 +1,6 @@ class Enterprise < ActiveRecord::Base TYPES = %w(full single profile) ENTERPRISE_SEARCH_RADIUS = 100 - self.inheritance_column = nil acts_as_gmappable :process_geocoding => false @@ -52,6 +51,7 @@ class Enterprise < ActiveRecord::Base validates :email, presence: true validates_presence_of :owner validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? } + validates_length_of :description, :maximum => 255 before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? } before_validation :set_unused_address_fields diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 241d1fafc7..fff57f6f21 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -86,6 +86,7 @@ describe Enterprise do subject { FactoryGirl.create(:distributor_enterprise, :address => FactoryGirl.create(:address)) } it { should validate_presence_of(:name) } it { should validate_presence_of(:email) } + it { should validate_length_of(:description, :maximum => 255) } it "requires an owner" do expect{ From 5e4186587117eb57917e5f85045e3be2a43152cb Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Tue, 14 Oct 2014 00:25:46 +1100 Subject: [PATCH 014/681] add utils angular module and navigation check factory --- app/assets/javascripts/admin/all.js | 1 + .../enterprise_controller.js.coffee | 10 +++-- .../admin/enterprises/enterprises.js.coffee | 2 +- .../utils/services/navigation_check.js.coffee | 37 +++++++++++++++++++ .../javascripts/admin/utils/utils.js.coffee | 1 + 5 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/admin/utils/services/navigation_check.js.coffee create mode 100644 app/assets/javascripts/admin/utils/utils.js.coffee diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index f8d3ceb721..7dc9d1116f 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -22,6 +22,7 @@ //= require ./payment_methods/payment_methods //= require ./products/products //= require ./shipping_methods/shipping_methods +//= require ./utils/utils //= require ./users/users //= require textAngular.min.js //= require textAngular-sanitize.min.js diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 045b6e3422..6558c789a2 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,11 +1,15 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, Enterprise, longDescription, PaymentMethods, ShippingMethods) -> + .controller "enterpriseCtrl", ($scope, longDescription, Enterprise, PaymentMethods, ShippingMethods, NavigationCheck) -> $scope.Enterprise = Enterprise.enterprise $scope.PaymentMethods = PaymentMethods.paymentMethods $scope.ShippingMethods = ShippingMethods.shippingMethods + # htmlVariable is used by textAngular wysiwyg for the long descrtiption. $scope.htmlVariable = longDescription - $scope.$on "$routeChangeStart", (event, newUrl, oldUrl) -> - event.preventDefault() + # Provide a callback for a warning message displayed when leaving the page. + navigationCallback = -> + "You are editing an enterprise!" + + NavigationCheck.register navigationCallback for payment_method in $scope.PaymentMethods payment_method.selected = payment_method.id in $scope.Enterprise.payment_method_ids diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index 9b67bc14f4..e1e43854d1 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprises", ["admin.payment_methods", "admin.shipping_methods", "admin.users", "textAngular"]) \ No newline at end of file +angular.module("admin.enterprises", ["admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular"]) \ No newline at end of file diff --git a/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee new file mode 100644 index 0000000000..1aea4e087f --- /dev/null +++ b/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee @@ -0,0 +1,37 @@ +angular.module("admin.utils") + .factory "NavigationCheck", ($window, $rootScope) -> + callbacks = [] + + # Action for regular browser navigation. + onBeforeUnloadHandler = ($event) -> + message = getMessage() + if message + ($event or $window.event).preventDefault() + message + + # Action for angular navigation. + locationChangeStartHandler = ($event) -> + message = getMessage() + if message and not $window.confirm(message) + $event.stopPropagation() if $event.stopPropagation + $event.preventDefault() if $event.preventDefault + $event.cancelBubble = true + $event.returnValue = false + + # Runs callback functions to retreive most recently added non-empty message. + getMessage = -> + message = null + message = callback() ? message for callback in callbacks + message + + register = (callback) -> + callbacks.push callback + + if $window.addEventListener + $window.addEventListener "beforeunload", onBeforeUnloadHandler + else + $window.onbeforeunload = onBeforeUnloadHandler + + $rootScope.$on "$locationChangeStart", locationChangeStartHandler + + return register: register diff --git a/app/assets/javascripts/admin/utils/utils.js.coffee b/app/assets/javascripts/admin/utils/utils.js.coffee new file mode 100644 index 0000000000..4d58ae930a --- /dev/null +++ b/app/assets/javascripts/admin/utils/utils.js.coffee @@ -0,0 +1 @@ +angular.module("admin.utils", []) \ No newline at end of file From 0d715ce61551b4acc9e54f165d200b5034d4409c Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 10 Oct 2014 19:36:37 +1100 Subject: [PATCH 015/681] split report permissions --- app/models/spree/ability_decorator.rb | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 5fdb4c2342..a62eae3935 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -1,6 +1,8 @@ class AbilityDecorator include CanCan::Ability + # All abilites are allocated from this initialiser, currently in 5 chunks. + # Spree also defines other abilities. def initialize(user) add_base_abilities user if is_new_user? user add_enterprise_management_abilities user if can_manage_enterprises? user @@ -9,19 +11,22 @@ class AbilityDecorator add_relationship_management_abilities user if can_manage_relationships? user end - + # New users have no enterprises. def is_new_user?(user) user.enterprises.blank? end + # Users can manage an enterprise if they have one. def can_manage_enterprises?(user) user.enterprises.present? end + # Users can manage products if they have an enterprise. def can_manage_products?(user) can_manage_enterprises? user end + # Users can manage orders if they have a sells own/any enterprise. def can_manage_orders?(user) ( user.enterprises.map(&:type) & %w(single full) ).any? end @@ -30,6 +35,7 @@ class AbilityDecorator can_manage_enterprises? user end + # New users can create an enterprise, and gain other permissions from doing this. def add_base_abilities(user) can [:create], Enterprise end @@ -47,6 +53,12 @@ class AbilityDecorator can [:read, :edit, :update, :bulk_update], Enterprise do |enterprise| user.enterprises.include? enterprise end + + # All enterprises can have fees, though possibly suppliers don't need them? + can [:index, :create], EnterpriseFee + can [:admin, :read, :edit, :bulk_update, :destroy], EnterpriseFee do |enterprise_fee| + user.enterprises.include? enterprise_fee.enterprise + end end def add_product_management_abilities(user) @@ -66,6 +78,9 @@ class AbilityDecorator can [:admin, :index, :read, :search], Spree::Taxon can [:admin, :index, :read, :create, :edit], Spree::Classification + + # Reports page + can [:admin, :index, :customers, :orders_and_fulfillment, :products_and_inventory], :report end def add_order_management_abilities(user) @@ -76,7 +91,7 @@ class AbilityDecorator # during the order creation process from the admin backend order.distributor.nil? || user.enterprises.include?(order.distributor) end - can [:admin, :bulk_management], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor?) + can [:admin, :bulk_management], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor) can [:admin, :create], Spree::LineItem can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Payment @@ -90,11 +105,6 @@ class AbilityDecorator end can [:for_order_cycle], Enterprise - can [:index, :create], EnterpriseFee - can [:admin, :read, :edit, :bulk_update, :destroy], EnterpriseFee do |enterprise_fee| - user.enterprises.include? enterprise_fee.enterprise - end - can [:admin, :index, :read, :create, :edit, :update], ExchangeVariant can [:admin, :index, :read, :create, :edit, :update], Exchange can [:admin, :index, :read, :create, :edit, :update], ExchangeFee @@ -111,7 +121,7 @@ class AbilityDecorator end # Reports page - can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report + can [:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report end From e44fed2ff0cb6d7ea6a8a733caf1a672c94ac9c7 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 12 Oct 2014 14:00:32 +1100 Subject: [PATCH 016/681] add authorization to reports listings on index page --- .../spree/admin/reports_controller_decorator.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 0abfd783b5..c7f5aa363a 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -608,12 +608,10 @@ Spree::Admin::ReportsController.class_eval do :payments => {:name => "Payment Reports", :description => "Reports for Payments"}, :orders_and_fulfillment => {:name => "Orders & Fulfillment Reports", :description => ''}, :customers => {:name => "Customers", :description => 'Customer details'}, - :products_and_inventory => {:name => "Products & Inventory", :description => ''} + :products_and_inventory => {:name => "Products & Inventory", :description => ''}, + :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" } } - if spree_current_user.has_spree_role? 'admin' - reports[:sales_total] = { :name => "Sales Total", :description => "Sales Total For All Orders" } - end - reports + reports.select { |action, details| can? action, :report } end def total_units(line_items) From 46df14c0d96290ba928d8d35decec94c22d7936e Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 12 Oct 2014 14:13:52 +1100 Subject: [PATCH 017/681] refator reports controller a little --- .../admin/reports_controller_decorator.rb | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index c7f5aa363a..3a3ca9de48 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -6,24 +6,6 @@ require 'open_food_network/order_grouper' require 'open_food_network/customers_report' Spree::Admin::ReportsController.class_eval do - # Fetches user's distributors, suppliers and order_cycles - before_filter :load_data, only: [:customers, :products_and_inventory] - - # Render a partial for orders and fulfillment description - respond_override :index => { :html => { :success => lambda { - @reports[:orders_and_fulfillment][:description] = - render_to_string(partial: 'orders_and_fulfillment_description', layout: false, locals: {report_types: REPORT_TYPES[:orders_and_fulfillment]}).html_safe - @reports[:products_and_inventory][:description] = - render_to_string(partial: 'products_and_inventory_description', layout: false, locals: {report_types: REPORT_TYPES[:products_and_inventory]}).html_safe - @reports[:customers][:description] = - render_to_string(partial: 'customers_description', layout: false, locals: {report_types: REPORT_TYPES[:customers]}).html_safe - } } } - - # OVERRIDING THIS so we use a method not a constant for available reports - def index - @reports = available_reports - respond_with(@reports) - end REPORT_TYPES = { orders_and_fulfillment: [ @@ -42,6 +24,26 @@ Spree::Admin::ReportsController.class_eval do ] } + # Fetches user's distributors, suppliers and order_cycles + before_filter :load_data, only: [:customers, :products_and_inventory] + + # Render a partial for orders and fulfillment description + respond_override :index => { :html => { :success => lambda { + @reports[:orders_and_fulfillment][:description] = + render_to_string(partial: 'orders_and_fulfillment_description', layout: false, locals: {report_types: REPORT_TYPES[:orders_and_fulfillment]}).html_safe + @reports[:products_and_inventory][:description] = + render_to_string(partial: 'products_and_inventory_description', layout: false, locals: {report_types: REPORT_TYPES[:products_and_inventory]}).html_safe + @reports[:customers][:description] = + render_to_string(partial: 'customers_description', layout: false, locals: {report_types: REPORT_TYPES[:customers]}).html_safe + } } } + + + # Overide spree reports list. + def index + @reports = authorized_reports + respond_with(@reports) + end + # This action is short because we refactored it like bosses def customers @report_types = REPORT_TYPES[:customers] @@ -601,7 +603,7 @@ Spree::Admin::ReportsController.class_eval do @order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC') end - def available_reports + def authorized_reports reports = { :orders_and_distributors => {:name => "Orders And Distributors", :description => "Orders with distributor details"}, :bulk_coop => {:name => "Bulk Co-Op", :description => "Reports for Bulk Co-Op orders"}, @@ -611,7 +613,8 @@ Spree::Admin::ReportsController.class_eval do :products_and_inventory => {:name => "Products & Inventory", :description => ''}, :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" } } - reports.select { |action, details| can? action, :report } + # Return only reports the user is authorized to view. + reports.select { |action| can? action, :report } end def total_units(line_items) From 1577c01a77300830196eb9115c6907dda2e0b965 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 12 Oct 2014 22:21:59 +1100 Subject: [PATCH 018/681] add reports abilities specs --- spec/models/spree/ability_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 2d2d1cfb01..62e1973e7f 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -151,6 +151,14 @@ module Spree should_not have_ability(:destroy, for: er2) end + it "should be able to read some reports" do + should have_ability([:admin, :index, :customers, :orders_and_fulfillment, :products_and_inventory], for: :reports) + end + + it "should not be able to read other reports" do + should_not have_ability([:sales_total, :group_buys, :bulk_coop, :payments], for: :reports) + end + end context "when is a distributor enterprise user" do @@ -237,6 +245,15 @@ module Spree it "should not be able to destroy enterprise relationships for other enterprises" do should_not have_ability(:destroy, for: er1) end + + it "should be able to read some reports" do + should have_ability([:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], for: :reports) + end + + it "should not be able to read other reports" do + should_not have_ability([:sales_total], for: :reports) + end + end context 'Order Cycle co-ordinator' do From 9343c3608b8999dd3be8d39883be60564023a96c Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 12 Oct 2014 22:22:35 +1100 Subject: [PATCH 019/681] allow supplier enterprise manager to see bulk coop reports --- app/models/spree/ability_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index a62eae3935..abf4aeac66 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -80,7 +80,7 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit], Spree::Classification # Reports page - can [:admin, :index, :customers, :orders_and_fulfillment, :products_and_inventory], :report + can [:admin, :index, :customers, :bulk_coop, :orders_and_fulfillment, :products_and_inventory], :report end def add_order_management_abilities(user) From fd7191f476549d187fdef676540b1f7b149f92d4 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Thu, 16 Oct 2014 05:26:38 +1100 Subject: [PATCH 020/681] add missing orders_and_distributors perm --- app/models/spree/ability_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index abf4aeac66..2d4831ec0b 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -121,7 +121,7 @@ class AbilityDecorator end # Reports page - can [:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report + can [:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], :report end From 245c1eb30582fdd8941e030abe29b51377319ece Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 17 Oct 2014 07:40:08 +1100 Subject: [PATCH 021/681] use a directive for navigation check to attach it to the page instead of the controller --- .../enterprises/controllers/enterprise_controller.js.coffee | 6 ++---- .../admin/utils/directives/navigation_check.js.coffee | 4 ++++ app/views/admin/enterprises/_ng_form.html.haml | 2 +- app/views/admin/enterprises/index.html.haml | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 6558c789a2..688bf45a99 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,16 +1,14 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, longDescription, Enterprise, PaymentMethods, ShippingMethods, NavigationCheck) -> + .controller "enterpriseCtrl", ($scope, longDescription, Enterprise, PaymentMethods, ShippingMethods) -> $scope.Enterprise = Enterprise.enterprise $scope.PaymentMethods = PaymentMethods.paymentMethods $scope.ShippingMethods = ShippingMethods.shippingMethods # htmlVariable is used by textAngular wysiwyg for the long descrtiption. $scope.htmlVariable = longDescription # Provide a callback for a warning message displayed when leaving the page. - navigationCallback = -> + $scope.navigationCallback = -> "You are editing an enterprise!" - NavigationCheck.register navigationCallback - for payment_method in $scope.PaymentMethods payment_method.selected = payment_method.id in $scope.Enterprise.payment_method_ids diff --git a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee new file mode 100644 index 0000000000..26964a677d --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.utils").directive "navigationCheck", (NavigationCheck)-> + link: ($scope) -> + # Define navigationCallback on the controller. + NavigationCheck.register($scope.navigationCallback) diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 18e9d6ed36..74b4347f16 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -3,7 +3,7 @@ = admin_inject_payment_methods = admin_inject_shipping_methods -.sixteen.columns.alpha{ ng: { app: 'admin.enterprises', controller: 'enterpriseCtrl' } } +.sixteen.columns.alpha{ ng: { app: 'admin.enterprises', controller: 'enterpriseCtrl' }, "navigation-check" => "" } .eleven.columns.alpha = render 'form', f: f .one.column   diff --git a/app/views/admin/enterprises/index.html.haml b/app/views/admin/enterprises/index.html.haml index 03e5335155..b2a215b1c6 100644 --- a/app/views/admin/enterprises/index.html.haml +++ b/app/views/admin/enterprises/index.html.haml @@ -8,7 +8,7 @@ = render 'admin/shared/enterprises_sub_menu' -= form_for @enterprise_set, :url => main_app.bulk_update_admin_enterprises_path do |f| += form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path do |f| %table#listing_enterprises.index %colgroup %col{style: "width: 25%;"}/ From 9536b3e764c7f3a5d82ae91b0c801f71e85d7410 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Fri, 17 Oct 2014 07:58:08 +1100 Subject: [PATCH 022/681] add navigation directive default --- .../admin/utils/directives/navigation_check.js.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee index 26964a677d..7eae3d69cd 100644 --- a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee @@ -1,4 +1,6 @@ angular.module("admin.utils").directive "navigationCheck", (NavigationCheck)-> link: ($scope) -> - # Define navigationCallback on the controller. + # Define navigationCallback on a controller in $scope, otherwise this default will be used: + $scope.navigationCallback ||= -> + "You will lose any unsaved work!" NavigationCheck.register($scope.navigationCallback) From 694dd0c32934702011dc3253545fee805e4ec573 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 17 Oct 2014 10:52:36 +1100 Subject: [PATCH 023/681] Enterprise user selling own produce only sees simple order cycle listing --- app/helpers/order_cycles_helper.rb | 4 ++++ app/views/admin/order_cycles/_row.html.haml | 20 +++++++++++--------- app/views/admin/order_cycles/index.html.haml | 14 ++++++++------ lib/open_food_network/permissions.rb | 6 ++++++ spec/features/admin/order_cycles_spec.rb | 20 ++++++++++++++++++++ 5 files changed, 49 insertions(+), 15 deletions(-) diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index dfd957a143..23ac74e944 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -62,6 +62,10 @@ module OrderCyclesHelper OrderCycle.active.with_distributor(@distributor).present? end + def order_cycles_simple_view + !OpenFoodNetwork::Permissions.new(spree_current_user).can_manage_complex_order_cycles? + end + def order_cycles_enabled? OpenFoodNetwork::FeatureToggle.enabled? :order_cycles end diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml index 75e84de025..e95adff38f 100644 --- a/app/views/admin/order_cycles/_row.html.haml +++ b/app/views/admin/order_cycles/_row.html.haml @@ -4,15 +4,17 @@ %td= link_to order_cycle.name, main_app.edit_admin_order_cycle_path(order_cycle) %td= order_cycle_form.text_field :orders_open_at, :class => 'datetimepicker', :value => order_cycle.orders_open_at %td= order_cycle_form.text_field :orders_close_at, :class => 'datetimepicker', :value => order_cycle.orders_close_at - %td.suppliers - - order_cycle.suppliers.managed_by(spree_current_user).each do |s| - = s.name - %br/ - %td= order_cycle.coordinator.name - %td.distributors - - order_cycle.distributors.managed_by(spree_current_user).each do |d| - = d.name - %br/ + + - unless order_cycles_simple_view + %td.suppliers + - order_cycle.suppliers.managed_by(spree_current_user).each do |s| + = s.name + %br/ + %td= order_cycle.coordinator.name + %td.distributors + - order_cycle.distributors.managed_by(spree_current_user).each do |d| + = d.name + %br/ %td.products - variant_images = capture do diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index 1fac06f507..f649a4ab47 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -13,9 +13,10 @@ %col %col{'style' => 'width: 20%;'} %col{'style' => 'width: 20%;'} - %col - %col - %col + - unless order_cycles_simple_view + %col + %col + %col %col %col %col @@ -25,9 +26,10 @@ %th Name %th Open %th Close - %th Suppliers - %th Coordinator - %th Distributors + - unless order_cycles_simple_view + %th Suppliers + %th Coordinator + %th Distributors %th Products %th.actions %th.actions diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 3d5df1e804..678e70941a 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -4,6 +4,12 @@ module OpenFoodNetwork @user = user end + def can_manage_complex_order_cycles? + managed_and_related_enterprises_with(:add_to_order_cycle).any? do |e| + e.sells == 'any' + end + end + # Find enterprises that an admin is allowed to add to an order cycle def order_cycle_enterprises managed_and_related_enterprises_with :add_to_order_cycle diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index febd966109..354be34671 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -572,7 +572,27 @@ feature %q{ occ = OrderCycle.last occ.name.should == "COPY OF #{oc.name}" end + end + + describe "as an enterprise user selling only my own produce" do + let(:user) { create_enterprise_user } + let(:enterprise) { create(:enterprise, is_primary_producer: true, sells: 'own') } + + use_short_wait + + before do + user.enterprise_roles.create! enterprise: enterprise + login_to_admin_as user + end + + it "shows me an index of order cycles without enterprise columns" do + create(:order_cycle, coordinator: enterprise) + visit admin_order_cycles_path + page.should_not have_selector 'th', text: 'SUPPLIERS' + page.should_not have_selector 'th', text: 'COORDINATOR' + page.should_not have_selector 'th', text: 'DISTRIBUTORS' + end end From f060da9c8db88368b41c144ba7753dd019223a91 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 17 Oct 2014 14:13:27 +1100 Subject: [PATCH 024/681] Rename angular app order_cycle -> admin.order_cycles --- app/assets/javascripts/admin/order_cycle.js.erb.coffee | 2 +- app/views/admin/order_cycles/edit.html.haml | 2 +- app/views/admin/order_cycles/new.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/order_cycle.js.erb.coffee b/app/assets/javascripts/admin/order_cycle.js.erb.coffee index 64f2550466..bcfb338a15 100644 --- a/app/assets/javascripts/admin/order_cycle.js.erb.coffee +++ b/app/assets/javascripts/admin/order_cycle.js.erb.coffee @@ -1,4 +1,4 @@ -angular.module('order_cycle', ['ngResource']) +angular.module('admin.order_cycles', ['ngResource']) .controller('AdminCreateOrderCycleCtrl', ['$scope', 'OrderCycle', 'Enterprise', 'EnterpriseFee', ($scope, OrderCycle, Enterprise, EnterpriseFee) -> $scope.enterprises = Enterprise.index() $scope.supplied_products = Enterprise.supplied_products diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index 9bf0a8ca31..b22f0cf51d 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -1,4 +1,4 @@ %h1 Edit Order Cycle -= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'order_cycle', 'ng-controller' => 'AdminEditOrderCycleCtrl', 'ng-submit' => 'submit()'} do |f| += form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => 'AdminEditOrderCycleCtrl', 'ng-submit' => 'submit()'} do |f| = render 'form', :f => f diff --git a/app/views/admin/order_cycles/new.html.haml b/app/views/admin/order_cycles/new.html.haml index c2e9c924f2..de1f229258 100644 --- a/app/views/admin/order_cycles/new.html.haml +++ b/app/views/admin/order_cycles/new.html.haml @@ -1,4 +1,4 @@ %h1 New Order Cycle -= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'order_cycle', 'ng-controller' => 'AdminCreateOrderCycleCtrl', 'ng-submit' => 'submit()'} do |f| += form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => 'AdminCreateOrderCycleCtrl', 'ng-submit' => 'submit()'} do |f| = render 'form', :f => f From 078d2bac118be128429b94396ce88e1376ebb68a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 17 Oct 2014 16:50:16 +1100 Subject: [PATCH 025/681] Extract factories from order cycles admin angular --- .../admin/order_cycle.js.erb.coffee | 229 ------------------ .../services/enterprise.js.coffee | 43 ++++ .../services/enterprise_fee.js.coffee | 18 ++ .../services/order_cycle.js.coffee | 168 +++++++++++++ 4 files changed, 229 insertions(+), 229 deletions(-) create mode 100644 app/assets/javascripts/admin/order_cycles/services/enterprise.js.coffee create mode 100644 app/assets/javascripts/admin/order_cycles/services/enterprise_fee.js.coffee create mode 100644 app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee diff --git a/app/assets/javascripts/admin/order_cycle.js.erb.coffee b/app/assets/javascripts/admin/order_cycle.js.erb.coffee index bcfb338a15..6a7d565bae 100644 --- a/app/assets/javascripts/admin/order_cycle.js.erb.coffee +++ b/app/assets/javascripts/admin/order_cycle.js.erb.coffee @@ -162,235 +162,6 @@ angular.module('admin.order_cycles', ['ngResource']) $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content') ]) - .factory('OrderCycle', ['$resource', '$window', ($resource, $window) -> - OrderCycle = $resource '/admin/order_cycles/:order_cycle_id.json', {}, { - 'index': { method: 'GET', isArray: true} - 'create': { method: 'POST'} - 'update': { method: 'PUT'}} - - { - order_cycle: - incoming_exchanges: [] - outgoing_exchanges: [] - coordinator_fees: [] - - loaded: false - - exchangeSelectedVariants: (exchange) -> - numActiveVariants = 0 - numActiveVariants++ for id, active of exchange.variants when active - numActiveVariants - - exchangeDirection: (exchange) -> - if this.order_cycle.incoming_exchanges.indexOf(exchange) == -1 then 'outgoing' else 'incoming' - - toggleProducts: (exchange) -> - exchange.showProducts = !exchange.showProducts - - setExchangeVariants: (exchange, variants, selected) -> - exchange.variants[variant] = selected for variant in variants - - addSupplier: (new_supplier_id) -> - this.order_cycle.incoming_exchanges.push({enterprise_id: new_supplier_id, incoming: true, active: true, variants: {}, enterprise_fees: []}) - - addDistributor: (new_distributor_id) -> - this.order_cycle.outgoing_exchanges.push({enterprise_id: new_distributor_id, incoming: false, active: true, variants: {}, enterprise_fees: []}) - - removeExchange: (exchange) -> - if exchange.incoming - incoming_index = this.order_cycle.incoming_exchanges.indexOf exchange - this.order_cycle.incoming_exchanges.splice(incoming_index, 1) - this.removeDistributionOfVariant(variant_id) for variant_id, active of exchange.variants when active - else - outgoing_index = this.order_cycle.outgoing_exchanges.indexOf exchange - this.order_cycle.outgoing_exchanges.splice(outgoing_index, 1) if outgoing_index > -1 - - addCoordinatorFee: -> - this.order_cycle.coordinator_fees.push({}) - - removeCoordinatorFee: (index) -> - this.order_cycle.coordinator_fees.splice(index, 1) - - addExchangeFee: (exchange) -> - exchange.enterprise_fees.push({}) - - removeExchangeFee: (exchange, index) -> - exchange.enterprise_fees.splice(index, 1) - - productSuppliedToOrderCycle: (product) -> - product_variant_ids = (variant.id for variant in product.variants) - variant_ids = [product.master_id].concat(product_variant_ids) - incomingExchangesVariants = this.incomingExchangesVariants() - - # TODO: This is an O(n^2) implementation of set intersection and thus is slooow. - # Use a better algorithm if needed. - # Also, incomingExchangesVariants is called every time, when it only needs to be - # called once per change to incoming variants. Some sort of caching? - ids = (variant_id for variant_id in variant_ids when incomingExchangesVariants.indexOf(variant_id) != -1) - ids.length > 0 - - variantSuppliedToOrderCycle: (variant) -> - this.incomingExchangesVariants().indexOf(variant.id) != -1 - - incomingExchangesVariants: -> - variant_ids = [] - - for exchange in this.order_cycle.incoming_exchanges - variant_ids.push(parseInt(id)) for id, active of exchange.variants when active - variant_ids - - participatingEnterpriseIds: -> - suppliers = (exchange.enterprise_id for exchange in this.order_cycle.incoming_exchanges) - distributors = (exchange.enterprise_id for exchange in this.order_cycle.outgoing_exchanges) - jQuery.unique(suppliers.concat(distributors)).sort() - - removeDistributionOfVariant: (variant_id) -> - for exchange in this.order_cycle.outgoing_exchanges - exchange.variants[variant_id] = false - - load: (order_cycle_id) -> - service = this - OrderCycle.get {order_cycle_id: order_cycle_id}, (oc) -> - angular.extend(service.order_cycle, oc) - service.order_cycle.incoming_exchanges = [] - service.order_cycle.outgoing_exchanges = [] - for exchange in service.order_cycle.exchanges - if exchange.incoming - angular.extend(exchange, {enterprise_id: exchange.sender_id, active: true}) - delete(exchange.receiver_id) - service.order_cycle.incoming_exchanges.push(exchange) - - else - angular.extend(exchange, {enterprise_id: exchange.receiver_id, active: true}) - delete(exchange.sender_id) - service.order_cycle.outgoing_exchanges.push(exchange) - - delete(service.order_cycle.exchanges) - service.loaded = true - - this.order_cycle - - create: -> - oc = new OrderCycle({order_cycle: this.dataForSubmit()}) - oc.$create (data) -> - if data['success'] - $window.location = '/admin/order_cycles' - else - console.log('Failed to create order cycle') - - update: -> - oc = new OrderCycle({order_cycle: this.dataForSubmit()}) - oc.$update {order_cycle_id: this.order_cycle.id}, (data) -> - if data['success'] - $window.location = '/admin/order_cycles' - else - console.log('Failed to update order cycle') - - dataForSubmit: -> - data = this.deepCopy() - data = this.removeInactiveExchanges(data) - data = this.translateCoordinatorFees(data) - data = this.translateExchangeFees(data) - data - - deepCopy: -> - data = angular.extend({}, this.order_cycle) - - # Copy exchanges - data.incoming_exchanges = (angular.extend {}, exchange for exchange in this.order_cycle.incoming_exchanges) if this.order_cycle.incoming_exchanges? - data.outgoing_exchanges = (angular.extend {}, exchange for exchange in this.order_cycle.outgoing_exchanges) if this.order_cycle.outgoing_exchanges? - - # Copy exchange fees - all_exchanges = (data.incoming_exchanges || []) + (data.outgoing_exchanges || []) - for exchange in all_exchanges - if exchange.enterprise_fees? - exchange.enterprise_fees = (angular.extend {}, fee for fee in exchange.enterprise_fees) - - data - - removeInactiveExchanges: (order_cycle) -> - order_cycle.incoming_exchanges = - (exchange for exchange in order_cycle.incoming_exchanges when exchange.active) - order_cycle.outgoing_exchanges = - (exchange for exchange in order_cycle.outgoing_exchanges when exchange.active) - order_cycle - - translateCoordinatorFees: (order_cycle) -> - order_cycle.coordinator_fee_ids = (fee.id for fee in order_cycle.coordinator_fees) - delete order_cycle.coordinator_fees - order_cycle - - translateExchangeFees: (order_cycle) -> - for exchange in order_cycle.incoming_exchanges - exchange.enterprise_fee_ids = (fee.id for fee in exchange.enterprise_fees) - delete exchange.enterprise_fees - for exchange in order_cycle.outgoing_exchanges - exchange.enterprise_fee_ids = (fee.id for fee in exchange.enterprise_fees) - delete exchange.enterprise_fees - order_cycle - }]) - - .factory('Enterprise', ['$resource', ($resource) -> - Enterprise = $resource('/admin/enterprises/for_order_cycle/:enterprise_id.json', {}, {'index': {method: 'GET', isArray: true}}) - - { - Enterprise: Enterprise - enterprises: {} - supplied_products: [] - loaded: false - - index: -> - service = this - - Enterprise.index (data) -> - for enterprise in data - service.enterprises[enterprise.id] = enterprise - - for product in enterprise.supplied_products - service.supplied_products.push(product) - - service.loaded = true - - this.enterprises - - suppliedVariants: (enterprise_id) -> - vs = (this.variantsOf(product) for product in this.enterprises[enterprise_id].supplied_products) - [].concat vs... - - variantsOf: (product) -> - if product.variants.length > 0 - variant.id for variant in product.variants - else - [product.master_id] - - totalVariants: (enterprise) -> - numVariants = 0 - - if enterprise - counts = for product in enterprise.supplied_products - numVariants += if product.variants.length == 0 then 1 else product.variants.length - - numVariants - }]) - - .factory('EnterpriseFee', ['$resource', ($resource) -> - EnterpriseFee = $resource('/admin/enterprise_fees/:enterprise_fee_id.json', {}, {'index': {method: 'GET', isArray: true}}) - - { - EnterpriseFee: EnterpriseFee - enterprise_fees: {} - loaded: false - - index: -> - service = this - EnterpriseFee.index (data) -> - service.enterprise_fees = data - service.loaded = true - - forEnterprise: (enterprise_id) -> - enterprise_fee for enterprise_fee in this.enterprise_fees when enterprise_fee.enterprise_id == enterprise_id - }]) - .directive('datetimepicker', ['$parse', ($parse) -> (scope, element, attrs) -> # using $parse instead of scope[attrs.datetimepicker] for cases diff --git a/app/assets/javascripts/admin/order_cycles/services/enterprise.js.coffee b/app/assets/javascripts/admin/order_cycles/services/enterprise.js.coffee new file mode 100644 index 0000000000..244d050aba --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/services/enterprise.js.coffee @@ -0,0 +1,43 @@ +angular.module('admin.order_cycles').factory('Enterprise', ($resource) -> + Enterprise = $resource('/admin/enterprises/for_order_cycle/:enterprise_id.json', {}, {'index': {method: 'GET', isArray: true}}) + + { + Enterprise: Enterprise + enterprises: {} + supplied_products: [] + loaded: false + + index: (callback=null) -> + service = this + + Enterprise.index (data) -> + for enterprise in data + service.enterprises[enterprise.id] = enterprise + + for product in enterprise.supplied_products + service.supplied_products.push(product) + + service.loaded = true + (callback || angular.noop)(service.enterprises) + + this.enterprises + + suppliedVariants: (enterprise_id) -> + vs = (this.variantsOf(product) for product in this.enterprises[enterprise_id].supplied_products) + [].concat vs... + + variantsOf: (product) -> + if product.variants.length > 0 + variant.id for variant in product.variants + else + [product.master_id] + + totalVariants: (enterprise) -> + numVariants = 0 + + if enterprise + counts = for product in enterprise.supplied_products + numVariants += if product.variants.length == 0 then 1 else product.variants.length + + numVariants + }) \ No newline at end of file diff --git a/app/assets/javascripts/admin/order_cycles/services/enterprise_fee.js.coffee b/app/assets/javascripts/admin/order_cycles/services/enterprise_fee.js.coffee new file mode 100644 index 0000000000..330d7c031e --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/services/enterprise_fee.js.coffee @@ -0,0 +1,18 @@ +angular.module('admin.order_cycles').factory('EnterpriseFee', ($resource) -> + EnterpriseFee = $resource('/admin/enterprise_fees/:enterprise_fee_id.json', {}, {'index': {method: 'GET', isArray: true}}) + + { + EnterpriseFee: EnterpriseFee + enterprise_fees: {} + loaded: false + + index: -> + service = this + EnterpriseFee.index (data) -> + service.enterprise_fees = data + service.loaded = true + + forEnterprise: (enterprise_id) -> + enterprise_fee for enterprise_fee in this.enterprise_fees when enterprise_fee.enterprise_id == enterprise_id + }) + diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee new file mode 100644 index 0000000000..5e46fdda50 --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -0,0 +1,168 @@ +angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) -> + OrderCycle = $resource '/admin/order_cycles/:order_cycle_id.json', {}, { + 'index': { method: 'GET', isArray: true} + 'create': { method: 'POST'} + 'update': { method: 'PUT'}} + + { + order_cycle: + incoming_exchanges: [] + outgoing_exchanges: [] + coordinator_fees: [] + + loaded: false + + exchangeSelectedVariants: (exchange) -> + numActiveVariants = 0 + numActiveVariants++ for id, active of exchange.variants when active + numActiveVariants + + exchangeDirection: (exchange) -> + if this.order_cycle.incoming_exchanges.indexOf(exchange) == -1 then 'outgoing' else 'incoming' + + toggleProducts: (exchange) -> + exchange.showProducts = !exchange.showProducts + + setExchangeVariants: (exchange, variants, selected) -> + exchange.variants[variant] = selected for variant in variants + + addSupplier: (new_supplier_id) -> + this.order_cycle.incoming_exchanges.push({enterprise_id: new_supplier_id, incoming: true, active: true, variants: {}, enterprise_fees: []}) + + addDistributor: (new_distributor_id) -> + this.order_cycle.outgoing_exchanges.push({enterprise_id: new_distributor_id, incoming: false, active: true, variants: {}, enterprise_fees: []}) + + removeExchange: (exchange) -> + if exchange.incoming + incoming_index = this.order_cycle.incoming_exchanges.indexOf exchange + this.order_cycle.incoming_exchanges.splice(incoming_index, 1) + this.removeDistributionOfVariant(variant_id) for variant_id, active of exchange.variants when active + else + outgoing_index = this.order_cycle.outgoing_exchanges.indexOf exchange + this.order_cycle.outgoing_exchanges.splice(outgoing_index, 1) if outgoing_index > -1 + + addCoordinatorFee: -> + this.order_cycle.coordinator_fees.push({}) + + removeCoordinatorFee: (index) -> + this.order_cycle.coordinator_fees.splice(index, 1) + + addExchangeFee: (exchange) -> + exchange.enterprise_fees.push({}) + + removeExchangeFee: (exchange, index) -> + exchange.enterprise_fees.splice(index, 1) + + productSuppliedToOrderCycle: (product) -> + product_variant_ids = (variant.id for variant in product.variants) + variant_ids = [product.master_id].concat(product_variant_ids) + incomingExchangesVariants = this.incomingExchangesVariants() + + # TODO: This is an O(n^2) implementation of set intersection and thus is slooow. + # Use a better algorithm if needed. + # Also, incomingExchangesVariants is called every time, when it only needs to be + # called once per change to incoming variants. Some sort of caching? + ids = (variant_id for variant_id in variant_ids when incomingExchangesVariants.indexOf(variant_id) != -1) + ids.length > 0 + + variantSuppliedToOrderCycle: (variant) -> + this.incomingExchangesVariants().indexOf(variant.id) != -1 + + incomingExchangesVariants: -> + variant_ids = [] + + for exchange in this.order_cycle.incoming_exchanges + variant_ids.push(parseInt(id)) for id, active of exchange.variants when active + variant_ids + + participatingEnterpriseIds: -> + suppliers = (exchange.enterprise_id for exchange in this.order_cycle.incoming_exchanges) + distributors = (exchange.enterprise_id for exchange in this.order_cycle.outgoing_exchanges) + jQuery.unique(suppliers.concat(distributors)).sort() + + removeDistributionOfVariant: (variant_id) -> + for exchange in this.order_cycle.outgoing_exchanges + exchange.variants[variant_id] = false + + load: (order_cycle_id) -> + service = this + OrderCycle.get {order_cycle_id: order_cycle_id}, (oc) -> + angular.extend(service.order_cycle, oc) + service.order_cycle.incoming_exchanges = [] + service.order_cycle.outgoing_exchanges = [] + for exchange in service.order_cycle.exchanges + if exchange.incoming + angular.extend(exchange, {enterprise_id: exchange.sender_id, active: true}) + delete(exchange.receiver_id) + service.order_cycle.incoming_exchanges.push(exchange) + + else + angular.extend(exchange, {enterprise_id: exchange.receiver_id, active: true}) + delete(exchange.sender_id) + service.order_cycle.outgoing_exchanges.push(exchange) + + delete(service.order_cycle.exchanges) + service.loaded = true + + this.order_cycle + + create: -> + oc = new OrderCycle({order_cycle: this.dataForSubmit()}) + oc.$create (data) -> + if data['success'] + $window.location = '/admin/order_cycles' + else + console.log('Failed to create order cycle') + + update: -> + oc = new OrderCycle({order_cycle: this.dataForSubmit()}) + oc.$update {order_cycle_id: this.order_cycle.id}, (data) -> + if data['success'] + $window.location = '/admin/order_cycles' + else + console.log('Failed to update order cycle') + + dataForSubmit: -> + data = this.deepCopy() + data = this.removeInactiveExchanges(data) + data = this.translateCoordinatorFees(data) + data = this.translateExchangeFees(data) + data + + deepCopy: -> + data = angular.extend({}, this.order_cycle) + + # Copy exchanges + data.incoming_exchanges = (angular.extend {}, exchange for exchange in this.order_cycle.incoming_exchanges) if this.order_cycle.incoming_exchanges? + data.outgoing_exchanges = (angular.extend {}, exchange for exchange in this.order_cycle.outgoing_exchanges) if this.order_cycle.outgoing_exchanges? + + # Copy exchange fees + all_exchanges = (data.incoming_exchanges || []) + (data.outgoing_exchanges || []) + for exchange in all_exchanges + if exchange.enterprise_fees? + exchange.enterprise_fees = (angular.extend {}, fee for fee in exchange.enterprise_fees) + + data + + removeInactiveExchanges: (order_cycle) -> + order_cycle.incoming_exchanges = + (exchange for exchange in order_cycle.incoming_exchanges when exchange.active) + order_cycle.outgoing_exchanges = + (exchange for exchange in order_cycle.outgoing_exchanges when exchange.active) + order_cycle + + translateCoordinatorFees: (order_cycle) -> + order_cycle.coordinator_fee_ids = (fee.id for fee in order_cycle.coordinator_fees) + delete order_cycle.coordinator_fees + order_cycle + + translateExchangeFees: (order_cycle) -> + for exchange in order_cycle.incoming_exchanges + exchange.enterprise_fee_ids = (fee.id for fee in exchange.enterprise_fees) + delete exchange.enterprise_fees + for exchange in order_cycle.outgoing_exchanges + exchange.enterprise_fee_ids = (fee.id for fee in exchange.enterprise_fees) + delete exchange.enterprise_fees + order_cycle + }) + From d8b648d531ff811870bd5802ee70475795f081b2 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 17 Oct 2014 16:51:03 +1100 Subject: [PATCH 026/681] Extract name and timing form from order cycles --- app/views/admin/order_cycles/_form.html.haml | 12 +----------- .../order_cycles/_name_and_timing_form.html.haml | 11 +++++++++++ 2 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 app/views/admin/order_cycles/_name_and_timing_form.html.haml diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index e859374a3b..82146fabdd 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -1,14 +1,4 @@ -= f.label :name -= f.text_field :name, 'ng-model' => 'order_cycle.name', 'required' => true -%br/ - -.date-field - = f.label :orders_open_at, 'Orders open' - = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at' -.date-field - = f.label :orders_close_at, 'Orders close' - = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at' -%br/ += render 'name_and_timing_form', f: f %h2 Incoming diff --git a/app/views/admin/order_cycles/_name_and_timing_form.html.haml b/app/views/admin/order_cycles/_name_and_timing_form.html.haml new file mode 100644 index 0000000000..437a6a44f7 --- /dev/null +++ b/app/views/admin/order_cycles/_name_and_timing_form.html.haml @@ -0,0 +1,11 @@ += f.label :name += f.text_field :name, 'ng-model' => 'order_cycle.name', 'required' => true +%br/ + +.date-field + = f.label :orders_open_at, 'Orders open' + = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at' +.date-field + = f.label :orders_close_at, 'Orders close' + = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at' +%br/ From deedafde9ac13b609c0d4b08852e27a89fcd1a09 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 17 Oct 2014 16:55:55 +1100 Subject: [PATCH 027/681] WIP: Simple order cycle form loads and displays correct info --- .../order_cycles/controllers/simple.js.coffee | 27 ++++++++ app/helpers/order_cycles_helper.rb | 2 +- .../admin/order_cycles/_simple_form.html.haml | 18 ++++++ app/views/admin/order_cycles/new.html.haml | 9 ++- spec/features/admin/order_cycles_spec.rb | 64 +++++++++++++++++-- 5 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee create mode 100644 app/views/admin/order_cycles/_simple_form.html.haml diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee new file mode 100644 index 0000000000..cdc8947f25 --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee @@ -0,0 +1,27 @@ +angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl", ($scope, OrderCycle, Enterprise, EnterpriseFee) -> + $scope.enterprises = Enterprise.index (enterprises) => + enterprise = enterprises[Object.keys(enterprises)[0]] + OrderCycle.addSupplier enterprise.id + OrderCycle.addDistributor enterprise.id + + OrderCycle.setExchangeVariants(OrderCycle.order_cycle.incoming_exchanges[0], + Enterprise.suppliedVariants(enterprise.id), true) + + OrderCycle.order_cycle.coordinator_id = enterprise.id + + $scope.enterprise_fees = EnterpriseFee.index() + + $scope.order_cycle = OrderCycle.order_cycle + + $scope.loaded = -> + Enterprise.loaded && EnterpriseFee.loaded + + $scope.removeDistributionOfVariant = angular.noop + + $scope.addCoordinatorFee = ($event) -> + $event.preventDefault() + OrderCycle.addCoordinatorFee() + + $scope.enterpriseFeesForEnterprise = (enterprise_id) -> + EnterpriseFee.forEnterprise(parseInt(enterprise_id)) + diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 23ac74e944..627d117368 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -63,7 +63,7 @@ module OrderCyclesHelper end def order_cycles_simple_view - !OpenFoodNetwork::Permissions.new(spree_current_user).can_manage_complex_order_cycles? + @order_cycles_simple_view ||= !OpenFoodNetwork::Permissions.new(spree_current_user).can_manage_complex_order_cycles? end def order_cycles_enabled? diff --git a/app/views/admin/order_cycles/_simple_form.html.haml b/app/views/admin/order_cycles/_simple_form.html.haml new file mode 100644 index 0000000000..37f7ab0152 --- /dev/null +++ b/app/views/admin/order_cycles/_simple_form.html.haml @@ -0,0 +1,18 @@ += render 'name_and_timing_form', f: f + += text_field_tag 'order_cycle_outgoing_exchange_0_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_time', 'placeholder' => 'Ready for (ie. Date / Time)', 'ng-model' => 'outgoing_exchange.pickup_time' +%br/ += text_field_tag 'order_cycle_outgoing_exchange_0_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_instructions', 'placeholder' => 'Pick-up instructions', 'ng-model' => 'outgoing_exchange.pickup_instructions' + +%div{ng: {repeat: "exchange in order_cycle.incoming_exchanges"}} + = render 'exchange_supplied_products_form' + += render 'coordinator_fees', f: f + +.actions + = f.submit @order_cycle.new_record? ? 'Create' : 'Update', 'ng-disabled' => '!loaded()' + %span{'ng-show' => 'loaded()'} + or + = link_to 'Cancel', main_app.admin_order_cycles_path + %span{'ng-hide' => 'loaded()'} Loading... + diff --git a/app/views/admin/order_cycles/new.html.haml b/app/views/admin/order_cycles/new.html.haml index de1f229258..716e537874 100644 --- a/app/views/admin/order_cycles/new.html.haml +++ b/app/views/admin/order_cycles/new.html.haml @@ -1,4 +1,9 @@ %h1 New Order Cycle -= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => 'AdminCreateOrderCycleCtrl', 'ng-submit' => 'submit()'} do |f| - = render 'form', :f => f +- ng_controller = order_cycles_simple_view ? 'AdminSimpleCreateOrderCycleCtrl' : 'AdminCreateOrderCycleCtrl' + += form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit()'} do |f| + - if order_cycles_simple_view + = render 'simple_form', f: f + - else + = render 'form', f: f diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 354be34671..c579b75f7c 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -131,16 +131,16 @@ feature %q{ page.should have_selector 'td.distributors', text: 'My distributor' # And it should have some fees - OrderCycle.last.exchanges.incoming.first.enterprise_fees.should == [supplier_fee] - OrderCycle.last.coordinator_fees.should == [coordinator_fee] - OrderCycle.last.exchanges.outgoing.first.enterprise_fees.should == [distributor_fee] + oc = OrderCycle.last + oc.exchanges.incoming.first.enterprise_fees.should == [supplier_fee] + oc.coordinator_fees.should == [coordinator_fee] + oc.exchanges.outgoing.first.enterprise_fees.should == [distributor_fee] # And it should have some variants selected - OrderCycle.last.exchanges.first.variants.count.should == 2 - OrderCycle.last.exchanges.last.variants.count.should == 2 + oc.exchanges.first.variants.count.should == 2 + oc.exchanges.last.variants.count.should == 2 # And my pickup time and instructions should have been saved - oc = OrderCycle.last exchange = oc.exchanges.where(:sender_id => oc.coordinator_id).first exchange.pickup_time.should == 'pickup time' exchange.pickup_instructions.should == 'pickup instructions' @@ -575,9 +575,14 @@ feature %q{ end - describe "as an enterprise user selling only my own produce" do + describe "simplified interface for enterprise users selling only their own produce" do let(:user) { create_enterprise_user } let(:enterprise) { create(:enterprise, is_primary_producer: true, sells: 'own') } + let!(:p1) { create(:simple_product, supplier: enterprise) } + let!(:p2) { create(:simple_product, supplier: enterprise) } + let!(:p3) { create(:simple_product, supplier: enterprise) } + let!(:v) { create(:variant, product: p3) } + let!(:fee) { create(:enterprise_fee, enterprise: enterprise, name: 'Coord fee') } use_short_wait @@ -593,6 +598,51 @@ feature %q{ page.should_not have_selector 'th', text: 'COORDINATOR' page.should_not have_selector 'th', text: 'DISTRIBUTORS' end + + it "creates order cycles", js: true do + # When I go to the new order cycle page + visit admin_order_cycles_path + click_link 'New Order Cycle' + + # And I fill in the basic fields + fill_in 'order_cycle_name', with: 'Plums & Avos' + fill_in 'order_cycle_orders_open_at', with: '2014-10-17 06:00:00' + fill_in 'order_cycle_orders_close_at', with: '2014-10-24 17:00:00' + fill_in 'order_cycle_outgoing_exchange_0_pickup_time', with: 'pickup time' + fill_in 'order_cycle_outgoing_exchange_0_pickup_instructions', with: 'pickup instructions' + + # Then my products / variants should already be selected + page.should have_checked_field "order_cycle_incoming_exchange_0_variants_#{p1.master.id}" + page.should have_checked_field "order_cycle_incoming_exchange_0_variants_#{p2.master.id}" + page.should have_checked_field "order_cycle_incoming_exchange_0_variants_#{v.id}" + + # When I unselect a product + uncheck "order_cycle_incoming_exchange_0_variants_#{p2.master.id}" + + # And I add a fee and save + click_button 'Add coordinator fee' + select 'Coord fee', from: 'order_cycle_coordinator_fee_0_id' + click_button 'Create' + + # Then my order cycle should have been created + page.should have_content 'Your order cycle has been created.' + page.should have_selector 'a', text: 'Plums & Avos' + page.should have_selector "input[value='2012-11-06 06:00:00 +1100']" + page.should have_selector "input[value='2012-11-13 17:00:00 +1100']" + + # And it should have some variants selected + oc = OrderCycle.last + oc.exchanges.incoming.first.variants.count.should == 2 + oc.exchanges.outgoing.first.variants.count.should == 2 + + # And it should have the fee + oc.coordinator_fees.should == [fee] + + # And my pickup time and instructions should have been saved + ex = oc.exchanges.outgoing.first + ex.pickup_time.should == 'pickup time' + ex.pickup_instructions.should == 'pickup instructions' + end end From 577cb06371632251bbaabfb237eb5f4c4f95b938 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 18 Oct 2014 01:42:29 +1100 Subject: [PATCH 028/681] make navCallback actually versatile --- .../controllers/enterprise_controller.js.coffee | 2 +- .../admin/utils/directives/navigation_check.js.coffee | 11 +++++++---- app/views/admin/enterprises/_ng_form.html.haml | 2 +- app/views/admin/order_cycles/index.html.haml | 2 -- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 688bf45a99..0fa452a8d6 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -6,7 +6,7 @@ angular.module("admin.enterprises") # htmlVariable is used by textAngular wysiwyg for the long descrtiption. $scope.htmlVariable = longDescription # Provide a callback for a warning message displayed when leaving the page. - $scope.navigationCallback = -> + $scope.enterpriseNavCallback = -> "You are editing an enterprise!" for payment_method in $scope.PaymentMethods diff --git a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee index 7eae3d69cd..6f5128d7bf 100644 --- a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee @@ -1,6 +1,9 @@ -angular.module("admin.utils").directive "navigationCheck", (NavigationCheck)-> - link: ($scope) -> +angular.module("admin.utils").directive "navCheck", (NavigationCheck)-> + restrict: 'A' + scope: + navCallback: '&' + link: (scope,element,attributes) -> # Define navigationCallback on a controller in $scope, otherwise this default will be used: - $scope.navigationCallback ||= -> + scope.navCallback ||= -> "You will lose any unsaved work!" - NavigationCheck.register($scope.navigationCallback) + NavigationCheck.register(scope.navCallback) diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 74b4347f16..e3ebe95877 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -3,7 +3,7 @@ = admin_inject_payment_methods = admin_inject_shipping_methods -.sixteen.columns.alpha{ ng: { app: 'admin.enterprises', controller: 'enterpriseCtrl' }, "navigation-check" => "" } +.sixteen.columns.alpha{ ng: { app: 'admin.enterprises', controller: 'enterpriseCtrl' }, nav: { check: '', callback: 'enterpriseNavCallback()' }} .eleven.columns.alpha = render 'form', f: f .one.column   diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index 1fac06f507..f9f832d83d 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -5,8 +5,6 @@ %li#new_order_cycle_link = button_link_to "New Order Cycle", main_app.new_admin_order_cycle_path, :icon => 'icon-plus', :id => 'admin_new_order_cycle_link' - - = form_for @order_cycle_set, :url => main_app.bulk_update_admin_order_cycles_path do |f| %table.index#listing_order_cycles %colgroup From 87cc1d6217ae2f033e839c9631d79547f5811f1c Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 18 Oct 2014 01:42:58 +1100 Subject: [PATCH 029/681] create db/backup folder if it dosnt exist --- script/backup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/script/backup.sh b/script/backup.sh index 462aef6abe..114a8352c6 100755 --- a/script/backup.sh +++ b/script/backup.sh @@ -4,4 +4,5 @@ set -e +mkdir -p db/backup ssh $1 "pg_dump -h localhost -U openfoodweb openfoodweb_production |gzip" > db/backup/$1-`date +%Y%m%d`.sql.gz From b71a40ae6dc75b7fc34389a86688cb2359ea847f Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 18 Oct 2014 03:26:57 +1100 Subject: [PATCH 030/681] show all permitted enterprises in order cycle summery --- app/views/admin/order_cycles/_row.html.haml | 4 ++-- spec/features/admin/order_cycles_spec.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml index 75e84de025..3b8c6f34bc 100644 --- a/app/views/admin/order_cycles/_row.html.haml +++ b/app/views/admin/order_cycles/_row.html.haml @@ -5,12 +5,12 @@ %td= order_cycle_form.text_field :orders_open_at, :class => 'datetimepicker', :value => order_cycle.orders_open_at %td= order_cycle_form.text_field :orders_close_at, :class => 'datetimepicker', :value => order_cycle.orders_close_at %td.suppliers - - order_cycle.suppliers.managed_by(spree_current_user).each do |s| + - order_cycle.suppliers.merge(OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises).each do |s| = s.name %br/ %td= order_cycle.coordinator.name %td.distributors - - order_cycle.distributors.managed_by(spree_current_user).each do |d| + - order_cycle.distributors.merge(OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises).each do |d| = d.name %br/ diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index febd966109..be91d442e1 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -471,6 +471,10 @@ feature %q{ # I should see only the order cycle I am coordinating page.should have_content oc_user_coordinating.name page.should_not have_content oc_for_other_user.name + + # The order cycle should show enterprises that I manage + page.should have_selector 'td.suppliers', text: supplier_managed.name + page.should have_selector 'td.distributors', text: distributor_managed.name # The order cycle should not show enterprises that I don't manage page.should_not have_selector 'td.suppliers', text: supplier_unmanaged.name From 2a7dd8b8f1eeeb331d929e2a64041c6fbd1ed04d Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 18 Oct 2014 06:53:38 +1100 Subject: [PATCH 031/681] update ability specs for reports --- spec/models/spree/ability_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 62e1973e7f..796aed2592 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -152,11 +152,11 @@ module Spree end it "should be able to read some reports" do - should have_ability([:admin, :index, :customers, :orders_and_fulfillment, :products_and_inventory], for: :reports) + should have_ability([:admin, :index, :customers, :bulk_coop, :orders_and_fulfillment, :products_and_inventory], for: :reports) end it "should not be able to read other reports" do - should_not have_ability([:sales_total, :group_buys, :bulk_coop, :payments], for: :reports) + should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors], for: :reports) end end @@ -247,7 +247,7 @@ module Spree end it "should be able to read some reports" do - should have_ability([:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], for: :reports) + should have_ability([:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], for: :reports) end it "should not be able to read other reports" do From 9e0f8100d935ee416c1c320034a47b6b2954a612 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 18 Oct 2014 08:28:08 +1100 Subject: [PATCH 032/681] only producers have product permissions --- app/models/spree/ability_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 4a77a2651b..1fb824d09c 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -23,7 +23,7 @@ class AbilityDecorator # Users can manage products if they have an enterprise. def can_manage_products?(user) - can_manage_enterprises? user + can_manage_enterprises?(user) && user.enterprises.is_primary_producer.present? end # Users can manage orders if they have a sells own/any enterprise. From 4308f7d09c4949ac1ee9de640e195567d19637f7 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 22 Oct 2014 09:43:10 +1100 Subject: [PATCH 033/681] Revert "use sql for migration" This reverts commit fec8f08966b4695a51a17867599c0c371fcbd685. --- ...140927005043_enterprise_config_refactor.rb | 53 +++++++++---------- db/schema.rb | 2 +- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/db/migrate/20140927005043_enterprise_config_refactor.rb b/db/migrate/20140927005043_enterprise_config_refactor.rb index f497e62f10..f9e8c936ce 100644 --- a/db/migrate/20140927005043_enterprise_config_refactor.rb +++ b/db/migrate/20140927005043_enterprise_config_refactor.rb @@ -4,19 +4,8 @@ class EnterpriseConfigRefactor < ActiveRecord::Migration add_index :enterprises, :sells add_index :enterprises, [:is_primary_producer, :sells] - # Combine is_distributor and type into sells. - db.select_values("SELECT id FROM enterprises").each do |enterprise_id| - distributor = db.select_values("SELECT is_distributor FROM enterprises WHERE id = #{db.quote(enterprise_id)}") - primary_producer = db.select_value("SELECT is_distributor FROM enterprises WHERE id = #{db.quote(enterprise_id)}") - type = db.select_value("SELECT type FROM enterprises WHERE id = #{db.quote(enterprise_id)}") - if type == "single" && (distributor || primary_producer) - sells = "own" - elsif !distributor || type == "profile" - sells = "none" - else - sells = "any" - end - db.update("UPDATE enterprises SET sells = #{db.quote(sells)} WHERE id = #{db.quote(enterprise_id)}") + Enterprise.all.each do |enterprise| + enterprise.update_attributes!({:sells => sells_what?(enterprise)}) end remove_column :enterprises, :type @@ -28,25 +17,33 @@ class EnterpriseConfigRefactor < ActiveRecord::Migration add_column :enterprises, :type, :string, null: false, default: 'profile' add_column :enterprises, :is_distributor, :boolean - # Combine is_distributor and type into sells. - db.select_values("SELECT id FROM enterprises").each do |enterprise_id| - sells = db.select_value("SELECT sells FROM enterprises WHERE id = #{db.quote(enterprise_id)}") - case sells - when "own" - type = "single" - when "any" - type = "full" - else - type = "profile" - end - distributor = sells != "none" - db.update("UPDATE enterprises SET type = #{db.quote(type)}, is_distributor = #{db.quote(distributor)} WHERE id = #{db.quote(enterprise_id)}") + Enterprise.all.each do |enterprise| + enterprise.update_attributes!({ + :type => type?(enterprise), + :is_distributor => distributes?(enterprise) + }) end remove_column :enterprises, :sells end - def db - ActiveRecord::Base.connection + def sells_what?(enterprise) + is_distributor = enterprise.read_attribute(:is_distributor) + is_primary_producer = enterprise.read_attribute(:is_primary_producer) + type = enterprise.read_attribute(:type) + return "own" if type == "single" && (is_distributor || is_primary_producer) + return "none" if !is_distributor || type == "profile" + return "any" + end + + def distributes?(enterprise) + enterprise.read_attribute(:sells) != "none" + end + + def type?(enterprise) + sells = enterprise.read_attribute(:sells) + return "profile" if sells == "none" + return "single" if sells == "own" + return "full" end end diff --git a/db/schema.rb b/db/schema.rb index 114dcd6b73..974dc8ac9b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -264,11 +264,11 @@ ActiveRecord::Schema.define(:version => 20141010043405) do t.string "instagram" t.string "linkedin" t.integer "owner_id", :null => false - t.string "sells", :default => "none", :null => false t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" + t.string "sells", :default => "none", :null => false end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" From b75af8d9fff0955c9f1f8fc0c18d22f12b278562 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 22 Oct 2014 10:49:23 +1100 Subject: [PATCH 034/681] Fix sells column coming out all 'none' --- db/migrate/20140927005043_enterprise_config_refactor.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/db/migrate/20140927005043_enterprise_config_refactor.rb b/db/migrate/20140927005043_enterprise_config_refactor.rb index f9e8c936ce..6b51ac6e50 100644 --- a/db/migrate/20140927005043_enterprise_config_refactor.rb +++ b/db/migrate/20140927005043_enterprise_config_refactor.rb @@ -1,9 +1,15 @@ class EnterpriseConfigRefactor < ActiveRecord::Migration + class Enterprise < ActiveRecord::Base + self.inheritance_column = nil + end + def up add_column :enterprises, :sells, :string, null: false, default: 'none' add_index :enterprises, :sells add_index :enterprises, [:is_primary_producer, :sells] + Enterprise.reset_column_information + Enterprise.all.each do |enterprise| enterprise.update_attributes!({:sells => sells_what?(enterprise)}) end @@ -17,6 +23,8 @@ class EnterpriseConfigRefactor < ActiveRecord::Migration add_column :enterprises, :type, :string, null: false, default: 'profile' add_column :enterprises, :is_distributor, :boolean + Enterprise.reset_column_information + Enterprise.all.each do |enterprise| enterprise.update_attributes!({ :type => type?(enterprise), From 8bf472e970be4f1f7b99d7c1934d5a5fbe0bf67e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 12:16:28 +1100 Subject: [PATCH 035/681] Split dashboard into single and multiple enterprise views --- .../admin/overview_controller_decorator.rb | 7 +++++ app/models/spree/user_decorator.rb | 4 +++ .../_multi_enterprise_dashboard.html.haml | 24 ++++++++++++++ .../_single_enterprise_dashboard.html.haml | 17 ++++++++++ .../spree/admin/overview/index.html.haml | 24 -------------- .../spree/admin/overview_controller_spec.rb | 31 +++++++++++++++++++ 6 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 app/views/spree/admin/overview/_multi_enterprise_dashboard.html.haml create mode 100644 app/views/spree/admin/overview/_single_enterprise_dashboard.html.haml delete mode 100644 app/views/spree/admin/overview/index.html.haml create mode 100644 spec/controllers/spree/admin/overview_controller_spec.rb diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index 4ae1c1b095..ddaddb5dc3 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -4,6 +4,13 @@ Spree::Admin::OverviewController.class_eval do @enterprises = Enterprise.managed_by(spree_current_user).order('is_primary_producer ASC, name') @product_count = Spree::Product.active.managed_by(spree_current_user).count @order_cycle_count = OrderCycle.active.managed_by(spree_current_user).count + + if spree_current_user.manages_one_enterprise? + @enterprise = @enterprises.first + render partial: "single_enterprise_dashboard" + else + render partial: "multi_enterprise_dashboard" + end end end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 06a413b469..e8b75f6466 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -19,6 +19,10 @@ Spree.user_class.class_eval do end end + def manages_one_enterprise? + enterprises.length == 1 + end + def send_signup_confirmation Spree::UserMailer.signup_confirmation(self).deliver end diff --git a/app/views/spree/admin/overview/_multi_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/_multi_enterprise_dashboard.html.haml new file mode 100644 index 0000000000..f230d7e44e --- /dev/null +++ b/app/views/spree/admin/overview/_multi_enterprise_dashboard.html.haml @@ -0,0 +1,24 @@ +%h1{ :style => 'margin-bottom: 30px'} Dashboard + +- if @enterprises.unconfirmed.any? + + = render partial: "unconfirmed" + + %hr + +- if @enterprises.empty? + + = render partial: "enterprises" + +- else + + - if can? :admin, Spree::Product + = render partial: "products" + + %div.two.columns +   + + - if can? :admin, OrderCycle + = render partial: "order_cycles" + + = render partial: "enterprises" \ No newline at end of file diff --git a/app/views/spree/admin/overview/_single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/_single_enterprise_dashboard.html.haml new file mode 100644 index 0000000000..e407c1dbe7 --- /dev/null +++ b/app/views/spree/admin/overview/_single_enterprise_dashboard.html.haml @@ -0,0 +1,17 @@ +-# - if @enterprise.sells == "unconfirmed" +-# %h1 Welcome to the Open Food Network + + +- if @enterprise.is_primary_producer + // Basic Producer + + // Producer with Shopfront + + // Coming soon - Full Hub + // Email us if you want this option + +- else + // Shop Profile + + // Coming soon - Full Hub + // Email us if you want this option diff --git a/app/views/spree/admin/overview/index.html.haml b/app/views/spree/admin/overview/index.html.haml deleted file mode 100644 index d83a71bc40..0000000000 --- a/app/views/spree/admin/overview/index.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -%h1{ :style => 'margin-bottom: 30px'} Dashboard - -- if @enterprises.unconfirmed.any? - - = render partial: "spree/admin/overview/unconfirmed" - - %hr - -- if @enterprises.empty? - - = render partial: "spree/admin/overview/enterprises" - -- else - - - if can? :admin, Spree::Product - = render partial: "spree/admin/overview/products" - - %div.two.columns -   - - - if can? :admin, OrderCycle - = render partial: "spree/admin/overview/order_cycles" - - = render partial: "spree/admin/overview/enterprises" diff --git a/spec/controllers/spree/admin/overview_controller_spec.rb b/spec/controllers/spree/admin/overview_controller_spec.rb new file mode 100644 index 0000000000..62443c2cfb --- /dev/null +++ b/spec/controllers/spree/admin/overview_controller_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Spree::Admin::OverviewController do + include AuthenticationWorkflow + context "loading overview" do + let(:user) { create_enterprise_user(enterprise_limit: 2) } + + before do + controller.stub spree_current_user: user + end + + context "when user own only one enterprise" do + let!(:enterprise) { create(:distributor_enterprise, owner: user) } + + it "renders the single enterprise dashboard" do + spree_get :index + response.should render_template partial: "_single_enterprise_dashboard" + end + end + + context "when user owns multiple enterprises" do + let!(:enterprise1) { create(:distributor_enterprise, owner: user) } + let!(:enterprise2) { create(:distributor_enterprise, owner: user) } + + it "renders the multi enterprise dashboard" do + spree_get :index + response.should render_template partial: "_multi_enterprise_dashboard" + end + end + end +end \ No newline at end of file From 3d938b345043c406752c96351d4c3a5d64a62c04 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 12:30:07 +1100 Subject: [PATCH 036/681] Shifting dashboard rendering from partials to templates --- app/controllers/spree/admin/overview_controller_decorator.rb | 4 ++-- ...shboard.html.haml => multi_enterprise_dashboard.html.haml} | 0 ...hboard.html.haml => single_enterprise_dashboard.html.haml} | 0 spec/controllers/spree/admin/overview_controller_spec.rb | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename app/views/spree/admin/overview/{_multi_enterprise_dashboard.html.haml => multi_enterprise_dashboard.html.haml} (100%) rename app/views/spree/admin/overview/{_single_enterprise_dashboard.html.haml => single_enterprise_dashboard.html.haml} (100%) diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index ddaddb5dc3..967cdf450d 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -7,9 +7,9 @@ Spree::Admin::OverviewController.class_eval do if spree_current_user.manages_one_enterprise? @enterprise = @enterprises.first - render partial: "single_enterprise_dashboard" + render "single_enterprise_dashboard" else - render partial: "multi_enterprise_dashboard" + render "multi_enterprise_dashboard" end end end diff --git a/app/views/spree/admin/overview/_multi_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml similarity index 100% rename from app/views/spree/admin/overview/_multi_enterprise_dashboard.html.haml rename to app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml diff --git a/app/views/spree/admin/overview/_single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml similarity index 100% rename from app/views/spree/admin/overview/_single_enterprise_dashboard.html.haml rename to app/views/spree/admin/overview/single_enterprise_dashboard.html.haml diff --git a/spec/controllers/spree/admin/overview_controller_spec.rb b/spec/controllers/spree/admin/overview_controller_spec.rb index 62443c2cfb..bac96afdf1 100644 --- a/spec/controllers/spree/admin/overview_controller_spec.rb +++ b/spec/controllers/spree/admin/overview_controller_spec.rb @@ -14,7 +14,7 @@ describe Spree::Admin::OverviewController do it "renders the single enterprise dashboard" do spree_get :index - response.should render_template partial: "_single_enterprise_dashboard" + response.should render_template "single_enterprise_dashboard" end end @@ -24,7 +24,7 @@ describe Spree::Admin::OverviewController do it "renders the multi enterprise dashboard" do spree_get :index - response.should render_template partial: "_multi_enterprise_dashboard" + response.should render_template "multi_enterprise_dashboard" end end end From 7ff4306991bdafcc72d0a070dcafeea980c63f24 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 13:13:21 +1100 Subject: [PATCH 037/681] Adding 'unspecified' option to 'sells' on Enterprise --- app/models/enterprise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 234d39d691..6286442733 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -1,5 +1,5 @@ class Enterprise < ActiveRecord::Base - SELLS = %w(none own any) + SELLS = %w(unconfirmed none own any) ENTERPRISE_SEARCH_RADIUS = 100 devise :confirmable, reconfirmable: true From 6798d394bbc37a27bb5083509636bbc40014fe52 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 14:50:22 +1100 Subject: [PATCH 038/681] Use unspecified rather than unconfirmed --- app/models/enterprise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 6286442733..5fddfe1002 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -1,5 +1,5 @@ class Enterprise < ActiveRecord::Base - SELLS = %w(unconfirmed none own any) + SELLS = %w(unspecified none own any) ENTERPRISE_SEARCH_RADIUS = 100 devise :confirmable, reconfirmable: true From 175e430a2bedd47ea9b8f550eeb88c55f1937364 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Wed, 22 Oct 2014 14:51:17 +1100 Subject: [PATCH 039/681] uss oop for nav check --- .../directives/navigation_check.js.coffee | 2 +- .../utils/services/navigation_check.js.coffee | 67 +++++++++++-------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee index 6f5128d7bf..95f52505eb 100644 --- a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee @@ -3,7 +3,7 @@ angular.module("admin.utils").directive "navCheck", (NavigationCheck)-> scope: navCallback: '&' link: (scope,element,attributes) -> - # Define navigationCallback on a controller in $scope, otherwise this default will be used: + # Define navigationCallback on a controller in scope, otherwise this default will be used: scope.navCallback ||= -> "You will lose any unsaved work!" NavigationCheck.register(scope.navCallback) diff --git a/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee index 1aea4e087f..ff1041474c 100644 --- a/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee +++ b/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee @@ -1,37 +1,46 @@ angular.module("admin.utils") .factory "NavigationCheck", ($window, $rootScope) -> - callbacks = [] + new class NavigationCheck + callbacks = [] + constructor: -> + if $window.addEventListener + $window.addEventListener "beforeunload", @onBeforeUnloadHandler + else + $window.onbeforeunload = @onBeforeUnloadHandler - # Action for regular browser navigation. - onBeforeUnloadHandler = ($event) -> - message = getMessage() - if message - ($event or $window.event).preventDefault() + $rootScope.$on "$locationChangeStart", @locationChangeStartHandler + + + # Action for regular browser navigation. + onBeforeUnloadHandler: ($event) => + message = @getMessage() + if message + ($event or $window.event).preventDefault() + message + + # Action for angular navigation. + locationChangeStartHandler: ($event) => + message = @getMessage() + if message and not $window.confirm(message) + $event.stopPropagation() if $event.stopPropagation + $event.preventDefault() if $event.preventDefault + $event.cancelBubble = true + $event.returnValue = false + + # Runs callback functions to retreive most recently added non-empty message. + getMessage: -> + message = null + message = callback() ? message for callback in callbacks message - # Action for angular navigation. - locationChangeStartHandler = ($event) -> - message = getMessage() - if message and not $window.confirm(message) - $event.stopPropagation() if $event.stopPropagation - $event.preventDefault() if $event.preventDefault - $event.cancelBubble = true - $event.returnValue = false + register: (callback) => + callbacks.push callback - # Runs callback functions to retreive most recently added non-empty message. - getMessage = -> - message = null - message = callback() ? message for callback in callbacks - message + clear: => + if $window.addEventListener + $window.removeEventListener "beforeunload", @onBeforeUnloadHandler + else + $window.onbeforeunload = null - register = (callback) -> - callbacks.push callback + $rootScope.$on "$locationChangeStart", null - if $window.addEventListener - $window.addEventListener "beforeunload", onBeforeUnloadHandler - else - $window.onbeforeunload = onBeforeUnloadHandler - - $rootScope.$on "$locationChangeStart", locationChangeStartHandler - - return register: register From afa6119ec1aa6e9f8a3d38e2e0620574aa02345a Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Wed, 22 Oct 2014 14:53:20 +1100 Subject: [PATCH 040/681] better coverage for enterprise abilities, hide products from non producers --- spec/models/spree/ability_spec.rb | 43 +++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index c44601bb86..caf09853f1 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -12,30 +12,63 @@ module Spree let(:enterprise_any) { create(:enterprise, sells: 'any') } let(:enterprise_own) { create(:enterprise, sells: 'own') } let(:enterprise_none) { create(:enterprise, sells: 'none') } + let(:enterprise_any_producer) { create(:enterprise, sells: 'any', is_primary_producer: true) } + let(:enterprise_own_producer) { create(:enterprise, sells: 'own', is_primary_producer: true) } + let(:enterprise_none_producer) { create(:enterprise, sells: 'none', is_primary_producer: true) } - context "as manager of a 'any' type enterprise" do + context "as manager of an enterprise who sells 'any'" do before do user.enterprise_roles.create! enterprise: enterprise_any end - it { subject.can_manage_products?(user).should be_true } + it { subject.can_manage_products?(user).should be_false } it { subject.can_manage_enterprises?(user).should be_true } it { subject.can_manage_orders?(user).should be_true } end - context "as manager of a 'own' type enterprise" do + context "as manager of an enterprise who sell 'own'" do before do user.enterprise_roles.create! enterprise: enterprise_own end + it { subject.can_manage_products?(user).should be_false } + it { subject.can_manage_enterprises?(user).should be_true } + it { subject.can_manage_orders?(user).should be_true } + end + + context "as manager of an enterprise who sells 'none'" do + before do + user.enterprise_roles.create! enterprise: enterprise_none + end + + it { subject.can_manage_products?(user).should be_false } + it { subject.can_manage_enterprises?(user).should be_true } + it { subject.can_manage_orders?(user).should be_false } + end + + context "as manager of a producer enterprise who sells 'any'" do + before do + user.enterprise_roles.create! enterprise: enterprise_any_producer + end + it { subject.can_manage_products?(user).should be_true } it { subject.can_manage_enterprises?(user).should be_true } it { subject.can_manage_orders?(user).should be_true } end - context "as manager of a 'none' type enterprise" do + context "as manager of a producer enterprise who sell 'own'" do before do - user.enterprise_roles.create! enterprise: enterprise_none + user.enterprise_roles.create! enterprise: enterprise_own_producer + end + + it { subject.can_manage_products?(user).should be_true } + it { subject.can_manage_enterprises?(user).should be_true } + it { subject.can_manage_orders?(user).should be_true } + end + + context "as manager of a producer enterprise who sells 'none'" do + before do + user.enterprise_roles.create! enterprise: enterprise_none_producer end it { subject.can_manage_products?(user).should be_true } From a53cbb677d0b1537f4d9582e4ccd80cddd30918e Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Wed, 22 Oct 2014 14:55:01 +1100 Subject: [PATCH 041/681] fix report typos --- spec/models/spree/ability_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index caf09853f1..e523cce755 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -185,11 +185,11 @@ module Spree end it "should be able to read some reports" do - should have_ability([:admin, :index, :customers, :bulk_coop, :orders_and_fulfillment, :products_and_inventory], for: :reports) + should have_ability([:admin, :index, :customers, :bulk_coop, :orders_and_fulfillment, :products_and_inventory], for: :report) end it "should not be able to read other reports" do - should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors], for: :reports) + should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors], for: :report) end end @@ -280,16 +280,16 @@ module Spree end it "should be able to read some reports" do - should have_ability([:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], for: :reports) + should have_ability([:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], for: :report) end it "should not be able to read other reports" do - should_not have_ability([:sales_total], for: :reports) + should_not have_ability([:sales_total], for: :report) end end - context 'Order Cycle co-ordinator, distriutor enterprise manager' do + context 'Order Cycle co-ordinator, distributor enterprise manager' do let (:user) do user = create(:user) user.spree_roles = [] From 565d6739b30553e59e92fa912b93895d3e99134e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 15:13:52 +1100 Subject: [PATCH 042/681] Add 'activated' scope to enterprises --- app/models/enterprise.rb | 1 + spec/models/enterprise_spec.rb | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 5fddfe1002..aa3a6043ef 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -63,6 +63,7 @@ class Enterprise < ActiveRecord::Base scope :visible, where(:visible => true) scope :confirmed, where('confirmed_at IS NOT NULL') scope :unconfirmed, where('confirmed_at IS NULL') + scope :activated, where('confirmed_at IS NOT NULL AND sells <> \'unspecified\'') scope :is_primary_producer, where(:is_primary_producer => true) scope :is_distributor, where('sells != ?', 'none') scope :supplying_variant_in, lambda { |variants| joins(:supplied_products => :variants_including_master).where('spree_variants.id IN (?)', variants).select('DISTINCT enterprises.*') } diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 151275ec71..6f80a33a4a 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -123,8 +123,8 @@ describe Enterprise do end describe "scopes" do - describe 'active' do - it 'find active enterprises' do + describe 'visible' do + it 'find visible enterprises' do d1 = create(:distributor_enterprise, visible: false) s1 = create(:supplier_enterprise) Enterprise.visible.should == [s1] @@ -153,6 +153,19 @@ describe Enterprise do end end + describe "activated" do + let!(:inactive_enterprise1) { create(:enterprise, sells: "unspecified", confirmed_at: Time.now) ;} + let!(:inactive_enterprise2) { create(:enterprise, sells: "none", confirmed_at: nil) } + let!(:active_enterprise) { create(:enterprise, sells: "none", confirmed_at: Time.now) } + + it "finds enterprises that have a sells property other than 'unspecified' and that are confirmed" do + activated_enterprises = Enterprise.activated + expect(activated_enterprises).to include active_enterprise + expect(activated_enterprises).to_not include inactive_enterprise1 + expect(activated_enterprises).to_not include inactive_enterprise2 + end + end + describe "distributors_with_active_order_cycles" do it "finds active distributors by order cycles" do s = create(:supplier_enterprise) From 96ff387d1fa571b924e4cf7307e455e254f5bfe0 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Wed, 22 Oct 2014 15:34:09 +1100 Subject: [PATCH 043/681] fix navigation check on submission --- .../controllers/enterprise_controller.js.coffee | 8 ++++++-- app/views/admin/enterprises/edit.html.haml | 2 +- app/views/admin/enterprises/new.html.haml | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 0fa452a8d6..cfcf6319af 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,11 +1,15 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, longDescription, Enterprise, PaymentMethods, ShippingMethods) -> + .controller "enterpriseCtrl", ($scope, longDescription, NavigationCheck, Enterprise, PaymentMethods, ShippingMethods) -> $scope.Enterprise = Enterprise.enterprise $scope.PaymentMethods = PaymentMethods.paymentMethods $scope.ShippingMethods = ShippingMethods.shippingMethods + $scope.navClear = NavigationCheck.clear # htmlVariable is used by textAngular wysiwyg for the long descrtiption. $scope.htmlVariable = longDescription - # Provide a callback for a warning message displayed when leaving the page. + + # Provide a callback for generating warning messages displayed before leaving the page. This is passed in + # from a directive "nav-check" in the page - if we pass it here it will be called in the test suite, + # and on all new uses of this contoller, and we might not want that . $scope.enterpriseNavCallback = -> "You are editing an enterprise!" diff --git a/app/views/admin/enterprises/edit.html.haml b/app/views/admin/enterprises/edit.html.haml index 6a8d48b24b..3c132bf5e2 100644 --- a/app/views/admin/enterprises/edit.html.haml +++ b/app/views/admin/enterprises/edit.html.haml @@ -4,7 +4,7 @@ Editing: = @enterprise.name -= form_for [main_app, :admin, @enterprise] do |f| += form_for [main_app, :admin, @enterprise], html: { "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl' , "nav-check" => '', "nav-callback" => 'enterpriseNavCallback()' } do |f| = render 'ng_form', f: f .twelve.columns.alpha = render partial: 'spree/admin/shared/edit_resource_links' diff --git a/app/views/admin/enterprises/new.html.haml b/app/views/admin/enterprises/new.html.haml index 32f6ea0a8d..3df3551f47 100644 --- a/app/views/admin/enterprises/new.html.haml +++ b/app/views/admin/enterprises/new.html.haml @@ -3,7 +3,7 @@ - content_for :page_title do New Enterprise -= form_for [main_app, :admin, @enterprise] do |f| - = render partial: 'ng_form', :locals => { f: f } += form_for [main_app, :admin, @enterprise], html: { "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl' , "nav-check" => '', "nav-callback" => 'enterpriseNavCallback()' } do |f| + = render 'ng_form', f: f .twelve.columns.alpha = render partial: 'spree/admin/shared/new_resource_links' From 45ed3a4cff29678dcacb183f7ed781ac56213d4a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 15:39:49 +1100 Subject: [PATCH 044/681] Only pushes activated enterprises through to the frontend --- app/helpers/injection_helper.rb | 4 ++-- spec/helpers/injection_helper_spec.rb | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index 046dd5d0eb..a168d0284d 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -1,6 +1,6 @@ module InjectionHelper def inject_enterprises - inject_json_ams "enterprises", Enterprise.all, Api::EnterpriseSerializer, active_distributors: @active_distributors + inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, active_distributors: @active_distributors end def inject_current_order @@ -28,7 +28,7 @@ module InjectionHelper def inject_spree_api_key render partial: "json/injection_ams", locals: {name: 'spreeApiKey', json: "'#{@spree_api_key.to_s}'"} end - + def inject_available_countries inject_json_ams "availableCountries", available_countries, Api::CountrySerializer end diff --git a/spec/helpers/injection_helper_spec.rb b/spec/helpers/injection_helper_spec.rb index 246f18d330..96d9279ef5 100644 --- a/spec/helpers/injection_helper_spec.rb +++ b/spec/helpers/injection_helper_spec.rb @@ -12,12 +12,17 @@ describe InjectionHelper do helper.inject_enterprises.should match enterprise.facebook end + it "only injects activated enterprises" do + inactive_enterprise = create(:enterprise, sells: 'unspecified') + helper.inject_enterprises.should_not match inactive_enterprise.name + end + it "injects shipping_methods" do sm = create(:shipping_method) helper.stub(:current_order).and_return order = create(:order) helper.stub_chain(:current_distributor, :shipping_methods, :uniq).and_return [sm] helper.inject_available_shipping_methods.should match sm.id.to_s - helper.inject_available_shipping_methods.should match sm.compute_amount(order).to_s + helper.inject_available_shipping_methods.should match sm.compute_amount(order).to_s end it "injects payment methods" do @@ -28,7 +33,7 @@ describe InjectionHelper do end it "injects current order" do - helper.stub(:current_order).and_return order = create(:order) + helper.stub(:current_order).and_return order = create(:order) helper.inject_current_order.should match order.id.to_s end From ecaa2e6a51750791cdcb0703020ee62508ba03f7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 15:53:25 +1100 Subject: [PATCH 045/681] Pull out override of 'visible' flag in enterprise serialiser --- app/serializers/api/enterprise_serializer.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index a2aa506d2f..3e6708b526 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -45,10 +45,6 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer has_one :address, serializer: Api::AddressSerializer - def visible - object.visible && object.confirmed? - end - def pickup object.shipping_methods.where(:require_ship_address => false).present? end From 82d33332ab98f6065e0499fead2b7940066d0bea Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 16:11:51 +1100 Subject: [PATCH 046/681] Adding shop_trial_start_date column to enterprises --- ...0141022050659_add_shop_trial_start_date_to_enterprises.rb | 5 +++++ db/schema.rb | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20141022050659_add_shop_trial_start_date_to_enterprises.rb diff --git a/db/migrate/20141022050659_add_shop_trial_start_date_to_enterprises.rb b/db/migrate/20141022050659_add_shop_trial_start_date_to_enterprises.rb new file mode 100644 index 0000000000..c9680f4e40 --- /dev/null +++ b/db/migrate/20141022050659_add_shop_trial_start_date_to_enterprises.rb @@ -0,0 +1,5 @@ +class AddShopTrialStartDateToEnterprises < ActiveRecord::Migration + def change + add_column :enterprises, :shop_trial_start_date, :datetime, default: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 974dc8ac9b..3c1fb143e4 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 => 20141010043405) do +ActiveRecord::Schema.define(:version => 20141022050659) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -264,11 +264,12 @@ ActiveRecord::Schema.define(:version => 20141010043405) do t.string "instagram" t.string "linkedin" t.integer "owner_id", :null => false + t.string "sells", :default => "none", :null => false t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" - t.string "sells", :default => "none", :null => false + t.datetime "shop_trial_start_date" end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" From 812002309412b174be26ac10e7ecd46446881652 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 16:33:43 +1100 Subject: [PATCH 047/681] Fixing indentation --- .../admin/overview/single_enterprise_dashboard.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index e407c1dbe7..31cdc72a0f 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -11,7 +11,7 @@ // Email us if you want this option - else - // Shop Profile + // Shop Profile - // Coming soon - Full Hub - // Email us if you want this option + // Coming soon - Full Hub + // Email us if you want this option From 7113875a45c8032a26d343841c95a8f71c6964c0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 16:49:32 +1100 Subject: [PATCH 048/681] Adding welcome page to overview controller --- .../spree/admin/overview_controller_decorator.rb | 6 +++++- app/views/spree/admin/overview/welcome.html.haml | 1 + .../spree/admin/overview_controller_spec.rb | 12 ++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 app/views/spree/admin/overview/welcome.html.haml diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index 967cdf450d..714c3996cd 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -7,7 +7,11 @@ Spree::Admin::OverviewController.class_eval do if spree_current_user.manages_one_enterprise? @enterprise = @enterprises.first - render "single_enterprise_dashboard" + if @enterprise.sells == "unspecified" + render "welcome" + else + render "single_enterprise_dashboard" + end else render "multi_enterprise_dashboard" end diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml new file mode 100644 index 0000000000..01f3f00c63 --- /dev/null +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -0,0 +1 @@ +Welcome \ No newline at end of file diff --git a/spec/controllers/spree/admin/overview_controller_spec.rb b/spec/controllers/spree/admin/overview_controller_spec.rb index bac96afdf1..d38d4b3b2e 100644 --- a/spec/controllers/spree/admin/overview_controller_spec.rb +++ b/spec/controllers/spree/admin/overview_controller_spec.rb @@ -16,6 +16,18 @@ describe Spree::Admin::OverviewController do spree_get :index response.should render_template "single_enterprise_dashboard" end + + context "when the enterprise sells property has not been set" do + before do + enterprise.sells = "unspecified" + enterprise.save + end + + it "renders the welcome page" do + spree_get :index + response.should render_template "welcome" + end + end end context "when user owns multiple enterprises" do From 8fb95769bf2550581a122de7d682814b293cd514 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 22 Oct 2014 17:07:45 +1100 Subject: [PATCH 049/681] Fix tested values in spec --- spec/features/admin/order_cycles_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index c579b75f7c..55623bfc18 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -627,8 +627,8 @@ feature %q{ # Then my order cycle should have been created page.should have_content 'Your order cycle has been created.' page.should have_selector 'a', text: 'Plums & Avos' - page.should have_selector "input[value='2012-11-06 06:00:00 +1100']" - page.should have_selector "input[value='2012-11-13 17:00:00 +1100']" + page.should have_selector "input[value='2014-10-17 06:00:00 +1100']" + page.should have_selector "input[value='2014-10-24 17:00:00 +1100']" # And it should have some variants selected oc = OrderCycle.last From 549ef4b79d33b9468fa27188a042c662fd73b2ee Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 22 Oct 2014 17:08:16 +1100 Subject: [PATCH 050/681] When saving an OC from the simple interface, include outgoing variants --- .../admin/order_cycles/controllers/simple.js.coffee | 3 +++ .../admin/order_cycles/services/order_cycle.js.coffee | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee index cdc8947f25..03f4128923 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee @@ -25,3 +25,6 @@ angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl $scope.enterpriseFeesForEnterprise = (enterprise_id) -> EnterpriseFee.forEnterprise(parseInt(enterprise_id)) + $scope.submit = -> + OrderCycle.mirrorIncomingToOutgoingProducts() + OrderCycle.create() diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index 5e46fdda50..b53c91257e 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -164,5 +164,14 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) exchange.enterprise_fee_ids = (fee.id for fee in exchange.enterprise_fees) delete exchange.enterprise_fees order_cycle - }) + # In the simple UI, we don't list outgoing products. Instead, all products are considered + # part of both incoming and outgoing enterprises. This method mirrors the former to the + # latter **for order cycles with a single incoming and outgoing exchange only**. + mirrorIncomingToOutgoingProducts: -> + incoming = this.order_cycle.incoming_exchanges[0] + outgoing = this.order_cycle.outgoing_exchanges[0] + + for id, active of incoming.variants + outgoing.variants[id] = active + }) From 28352be72946015f3c91a8354db528a1eca440ca Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 22 Oct 2014 17:17:05 +1100 Subject: [PATCH 051/681] Save pickup time and instructions --- .../javascripts/admin/order_cycles/controllers/simple.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee index 03f4128923..83f0999b30 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee @@ -3,6 +3,7 @@ angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl enterprise = enterprises[Object.keys(enterprises)[0]] OrderCycle.addSupplier enterprise.id OrderCycle.addDistributor enterprise.id + $scope.outgoing_exchange = OrderCycle.order_cycle.outgoing_exchanges[0] OrderCycle.setExchangeVariants(OrderCycle.order_cycle.incoming_exchanges[0], Enterprise.suppliedVariants(enterprise.id), true) From 69614d6cf149782c47a88e3e02a92210081fbef9 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Wed, 22 Oct 2014 17:23:06 +1100 Subject: [PATCH 052/681] fix accordion jumps --- app/views/checkout/_shipping.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index 27089527a2..5ba5ed15e4 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -83,4 +83,4 @@ .row .small-12.columns.text-right - %button.primary{"ng-disabled" => "shipping.$invalid", "ng-click" => "next($event)", "ofn-focus" => "accordion['shipping']"} Next + %button.primary{"ng-disabled" => "shipping.$invalid", "ng-click" => "next($event)"} Next From 3e61aa72677213f5a063f761d5cc38c9212cc7f5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 22 Oct 2014 17:26:22 +1100 Subject: [PATCH 053/681] Simple interface remove coordinator fee --- .../admin/order_cycles/controllers/simple.js.coffee | 4 ++++ spec/features/admin/order_cycles_spec.rb | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee index 83f0999b30..4499cd833b 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee @@ -23,6 +23,10 @@ angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl $event.preventDefault() OrderCycle.addCoordinatorFee() + $scope.removeCoordinatorFee = ($event, index) -> + $event.preventDefault() + OrderCycle.removeCoordinatorFee(index) + $scope.enterpriseFeesForEnterprise = (enterprise_id) -> EnterpriseFee.forEnterprise(parseInt(enterprise_id)) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 55623bfc18..153f34f4a7 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -621,6 +621,11 @@ feature %q{ # And I add a fee and save click_button 'Add coordinator fee' + click_button 'Add coordinator fee' + click_link 'order_cycle_coordinator_fee_1_remove' + page.should have_select 'order_cycle_coordinator_fee_0_id' + page.should_not have_select 'order_cycle_coordinator_fee_1_id' + select 'Coord fee', from: 'order_cycle_coordinator_fee_0_id' click_button 'Create' From 069b9ce91d89cf489488ca0950b8b69b7df4d656 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Wed, 22 Oct 2014 17:49:31 +1100 Subject: [PATCH 054/681] fix email length validation spec --- spec/models/enterprise_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 7adeb08d2d..206be1e131 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -105,7 +105,7 @@ describe Enterprise do subject { FactoryGirl.create(:distributor_enterprise, :address => FactoryGirl.create(:address)) } it { should validate_presence_of(:name) } it { should validate_presence_of(:email) } - it { should validate_length_of(:description, :maximum => 255) } + it { should ensure_length_of(:description).is_at_most(255) } it "requires an owner" do expect{ From fef97314cb593fdf307b1a10e65bfb61d6884dc6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 18:26:18 +1100 Subject: [PATCH 055/681] Adding basic layout of welcome page for new users --- app/assets/stylesheets/admin/welcome.css.sass | 49 +++++++++++++++ .../admin/overview_controller_decorator.rb | 2 +- .../spree/admin/overview/welcome.html.haml | 61 ++++++++++++++++++- app/views/spree/layouts/bare_admin.html.haml | 23 +++++++ 4 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 app/assets/stylesheets/admin/welcome.css.sass create mode 100644 app/views/spree/layouts/bare_admin.html.haml diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass new file mode 100644 index 0000000000..a575a96666 --- /dev/null +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -0,0 +1,49 @@ +#welcome_page + color: #000000 + h1, h2, h3, h4, h5, h6 + color: #000000 + + .plain_text + font-size: 16px + + + #welcome + text-align: center + margin: 50px 0px + h1 + margin-bottom: 10px + + #next_steps + text-align: center + margin-bottom: 50px + h1 + margin-bottom: 10px + + .options + .option + .selector + border: 3px solid black + border-radius: 12px 12px 0px 0px + text-align: center + margin-bottom: 20px + .top + min-height: 113px + padding: 15px 8px + h2 + margin-bottom: 10px + .bottom + border-top: 3px solid black + padding: 8px 0px + + &.disabled + color: #b0b0b0 + .selector + border-color: #b0b0b0 + h1, h2, h3, h4, h5, h6 + color: #b0b0b0 + .bottom + border-top-color: #b0b0b0 + + .description + font-size: 16px + text-align: justify \ No newline at end of file diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index 714c3996cd..74535d5dd7 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -8,7 +8,7 @@ Spree::Admin::OverviewController.class_eval do if spree_current_user.manages_one_enterprise? @enterprise = @enterprises.first if @enterprise.sells == "unspecified" - render "welcome" + render "welcome", layout: "spree/layouts/bare_admin" else render "single_enterprise_dashboard" end diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index 01f3f00c63..a65909896e 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -1 +1,60 @@ -Welcome \ No newline at end of file +#welcome_page.sixteen.columns.alpha{ ng: { controller: 'WelcomeCtrl' } } + #welcome + %h1 Welcome to the Open Food Network! + %h6= "You have successfully created a#{" producer" if @enterprise.is_primary_producer} profile" + + #next_steps + %h1 Next Steps + .plain_text + Choose how you would like to use + %br/ + the Open Food Network + + .options.sixteen.columns.alpha + - if @enterprise.is_primary_producer + .basic_producer.option.one-third.column.alpha + .selector + .top + %h2 Basic Producer + .plain_text Supply only + .bottom + %h5 ALWAYS FREE + .description + You want to use OFN as a place for people to find and contact you. + You can also add your products, allowing customers to see your product range and allowing you to act as a supplier to other shopfronts. + + .producer_shop.option.one-third.column + .selector + .top + %h2 Producer with Shop + .plain_text Sell your products through an OFN shopfront + .bottom + %h5 30 DAY TRIAL + .description + Test out having your own shopfront with full access to all Shopfront features for 30 days. + After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. + + .full_hub.option.one-third.column.omega.disabled + .selector + .top.center + %h2 Full Hub + .plain_text Sell other producers' products through an OFN shopfront + .bottom.center + %h5 COMING SOON + .description + You want to offer other producers' products for sale as well as your own, establish and coodinate trading relationships with other enterprises and + A full hub subscription gives you access to our complete suite of features for managing your food enterprise. + + - else + .shop_profile.option.one-third.column.alpha + .button + .top.center + %h2 Shop Profile + .plain_text Get a listing + .bottom.center + %h5 ALWAYS FREE + .description + You want to use OFN as a place for people to find and contact you. + + // Coming soon - Full Hub + // Email us if you want this option \ No newline at end of file diff --git a/app/views/spree/layouts/bare_admin.html.haml b/app/views/spree/layouts/bare_admin.html.haml new file mode 100644 index 0000000000..8440252350 --- /dev/null +++ b/app/views/spree/layouts/bare_admin.html.haml @@ -0,0 +1,23 @@ +%html{ lang: "en" } + %head{"data-hook" => "admin_inside_head"}= render :partial => 'spree/admin/shared/head' + %body.admin{"data-ajax-root-path" => spree.root_path} + #wrapper{"data-hook" => ""} + - if flash[:error] + .flash.error= flash[:error] + - if notice + .flash.notice= notice + - if flash[:success] + .flash.success= flash[:success] + #progress + .wrapper + #spinner + .progress-message + = t(:loading) + \... + = render :partial => 'spree/admin/shared/alert', :collection => session[:alerts] + .container + .row + #content{"data-hook" => ""} + %div{:class => "sixteen columns"} + = yield + %div{"data-hook" => "admin_footer_scripts"} \ No newline at end of file From 262972a911a9669a09ef4cfbf1af034e00a77feb Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 19:02:39 +1100 Subject: [PATCH 056/681] Adding basic angular to welcome form --- app/assets/javascripts/admin/all.js | 1 + .../welcome/controllers/welcome_controller.js.coffee | 3 +++ app/assets/javascripts/admin/welcome/welcome.js.coffee | 1 + app/assets/stylesheets/admin/welcome.css.sass | 10 ++++++++++ app/views/spree/admin/overview/welcome.html.haml | 8 ++++---- 5 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee create mode 100644 app/assets/javascripts/admin/welcome/welcome.js.coffee diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index f8d3ceb721..c674b1daae 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -23,6 +23,7 @@ //= require ./products/products //= require ./shipping_methods/shipping_methods //= require ./users/users +//= require ./welcome/welcome //= require textAngular.min.js //= require textAngular-sanitize.min.js diff --git a/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee b/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee new file mode 100644 index 0000000000..f9d43b672e --- /dev/null +++ b/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.welcome") + .controller "welcomeCtrl", ($scope) -> + $scope.sells = "unspecified" \ No newline at end of file diff --git a/app/assets/javascripts/admin/welcome/welcome.js.coffee b/app/assets/javascripts/admin/welcome/welcome.js.coffee new file mode 100644 index 0000000000..ccb0fb5d0a --- /dev/null +++ b/app/assets/javascripts/admin/welcome/welcome.js.coffee @@ -0,0 +1 @@ +angular.module("admin.welcome", []) \ No newline at end of file diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass index a575a96666..37f70669fe 100644 --- a/app/assets/stylesheets/admin/welcome.css.sass +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -35,6 +35,16 @@ border-top: 3px solid black padding: 8px 0px + &:hover + cursor: 'pointer' + + &.selected + color: #ffffff + background-color: #ff4949 + border-color: #000000 + h1, h2, h3, h4, h5, h6 + color: #ffffff + &.disabled color: #b0b0b0 .selector diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index a65909896e..925d47d676 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -1,4 +1,4 @@ -#welcome_page.sixteen.columns.alpha{ ng: { controller: 'WelcomeCtrl' } } +#welcome_page.sixteen.columns.alpha{ ng: { app: "admin.welcome", controller: 'welcomeCtrl' } } #welcome %h1 Welcome to the Open Food Network! %h6= "You have successfully created a#{" producer" if @enterprise.is_primary_producer} profile" @@ -13,7 +13,7 @@ .options.sixteen.columns.alpha - if @enterprise.is_primary_producer .basic_producer.option.one-third.column.alpha - .selector + .selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } .top %h2 Basic Producer .plain_text Supply only @@ -24,7 +24,7 @@ You can also add your products, allowing customers to see your product range and allowing you to act as a supplier to other shopfronts. .producer_shop.option.one-third.column - .selector + .selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } .top %h2 Producer with Shop .plain_text Sell your products through an OFN shopfront @@ -47,7 +47,7 @@ - else .shop_profile.option.one-third.column.alpha - .button + .selector{ ng: { class: "{selected: sells=='none'}" } } .top.center %h2 Shop Profile .plain_text Get a listing From d47db903fb6535010ef6fb54b3cbad5b0b6a47d9 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 22 Oct 2014 19:12:51 +1100 Subject: [PATCH 057/681] Add basic styling to simple order cycle interface --- .../_name_and_timing_form.html.haml | 24 +++++++++++-------- .../admin/order_cycles/_simple_form.html.haml | 18 ++++++++++---- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/views/admin/order_cycles/_name_and_timing_form.html.haml b/app/views/admin/order_cycles/_name_and_timing_form.html.haml index 437a6a44f7..ed24d20f39 100644 --- a/app/views/admin/order_cycles/_name_and_timing_form.html.haml +++ b/app/views/admin/order_cycles/_name_and_timing_form.html.haml @@ -1,11 +1,15 @@ -= f.label :name -= f.text_field :name, 'ng-model' => 'order_cycle.name', 'required' => true -%br/ +.row + .alpha.two.columns + = f.label :name + .fourteen.columns.omega + = f.text_field :name, 'ng-model' => 'order_cycle.name', 'required' => true -.date-field - = f.label :orders_open_at, 'Orders open' - = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at' -.date-field - = f.label :orders_close_at, 'Orders close' - = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at' -%br/ +.row + .alpha.two.columns + = f.label :orders_open_at, 'Orders open' + .six.columns + = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at' + .two.columns + = f.label :orders_close_at, 'Orders close' + .six.columns.omega + = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at' diff --git a/app/views/admin/order_cycles/_simple_form.html.haml b/app/views/admin/order_cycles/_simple_form.html.haml index 37f7ab0152..912631bb37 100644 --- a/app/views/admin/order_cycles/_simple_form.html.haml +++ b/app/views/admin/order_cycles/_simple_form.html.haml @@ -1,11 +1,19 @@ = render 'name_and_timing_form', f: f -= text_field_tag 'order_cycle_outgoing_exchange_0_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_time', 'placeholder' => 'Ready for (ie. Date / Time)', 'ng-model' => 'outgoing_exchange.pickup_time' -%br/ -= text_field_tag 'order_cycle_outgoing_exchange_0_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_instructions', 'placeholder' => 'Pick-up instructions', 'ng-model' => 'outgoing_exchange.pickup_instructions' +.row + .alpha.two.columns + = label_tag 'Pickup time' + .six.columns + = text_field_tag 'order_cycle_outgoing_exchange_0_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_time', 'placeholder' => 'Ready for (ie. Date / Time)', 'ng-model' => 'outgoing_exchange.pickup_time' + .two.columns + = label_tag 'Pickup instructions' + .six.columns.omega + = text_field_tag 'order_cycle_outgoing_exchange_0_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_instructions', 'placeholder' => 'Pick-up instructions', 'ng-model' => 'outgoing_exchange.pickup_instructions' -%div{ng: {repeat: "exchange in order_cycle.incoming_exchanges"}} - = render 'exchange_supplied_products_form' +%table.exchanges + %tbody{ng: {repeat: "exchange in order_cycle.incoming_exchanges"}} + %tr.products + = render 'exchange_supplied_products_form' = render 'coordinator_fees', f: f From f60a9d7bd5988a37457f5fa0c1f6d8c37f420d8d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 22 Oct 2014 19:13:17 +1100 Subject: [PATCH 058/681] Fiddling with styling --- app/assets/stylesheets/admin/welcome.css.sass | 7 +++++-- app/views/spree/admin/overview/welcome.html.haml | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass index 37f70669fe..11a15a6314 100644 --- a/app/assets/stylesheets/admin/welcome.css.sass +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -3,10 +3,13 @@ h1, h2, h3, h4, h5, h6 color: #000000 + .big + font-weight: bold + font-size: 36px + .plain_text font-size: 16px - #welcome text-align: center margin: 50px 0px @@ -40,7 +43,7 @@ &.selected color: #ffffff - background-color: #ff4949 + background-color: #ff4444 border-color: #000000 h1, h2, h3, h4, h5, h6 color: #ffffff diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index 925d47d676..6aef4ee027 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -1,7 +1,7 @@ #welcome_page.sixteen.columns.alpha{ ng: { app: "admin.welcome", controller: 'welcomeCtrl' } } #welcome - %h1 Welcome to the Open Food Network! - %h6= "You have successfully created a#{" producer" if @enterprise.is_primary_producer} profile" + .big Welcome to the Open Food Network! + %h4= "You have successfully created a#{" producer" if @enterprise.is_primary_producer} profile" #next_steps %h1 Next Steps From 66e13d40f4267b49828f0f8573be9ba6c2d10ebb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 22 Oct 2014 19:17:28 +1100 Subject: [PATCH 059/681] Select all works on simple order cycles interface --- .../admin/order_cycles/controllers/simple.js.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee index 4499cd833b..728b27fb4a 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee @@ -19,6 +19,12 @@ angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl $scope.removeDistributionOfVariant = angular.noop + $scope.setExchangeVariants = (exchange, variants, selected) -> + OrderCycle.setExchangeVariants(exchange, variants, selected) + + $scope.suppliedVariants = (enterprise_id) -> + Enterprise.suppliedVariants(enterprise_id) + $scope.addCoordinatorFee = ($event) -> $event.preventDefault() OrderCycle.addCoordinatorFee() From f74ec03cefe3b9002aa4d68b9da5d25d9d2f030f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 09:26:55 +1100 Subject: [PATCH 060/681] Rename --- .../controllers/{simple.js.coffee => simple_edit.js.coffee} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/admin/order_cycles/controllers/{simple.js.coffee => simple_edit.js.coffee} (100%) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee similarity index 100% rename from app/assets/javascripts/admin/order_cycles/controllers/simple.js.coffee rename to app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee From 2ad823a79d3da5e6140831a4019182b479bf0927 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 09:39:23 +1100 Subject: [PATCH 061/681] Fix JS specs --- spec/javascripts/unit/order_cycle_spec.js.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 2a8d001804..7ecd2b4782 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -38,7 +38,7 @@ describe 'OrderCycle controllers', -> index: jasmine.createSpy('index').andReturn('enterprise fees list') forEnterprise: jasmine.createSpy('forEnterprise').andReturn('enterprise fees for enterprise') - module('order_cycle') + module('admin.order_cycles') inject ($controller) -> ctrl = $controller 'AdminCreateOrderCycleCtrl', {$scope: scope, OrderCycle: OrderCycle, Enterprise: Enterprise, EnterpriseFee: EnterpriseFee} @@ -198,7 +198,7 @@ describe 'OrderCycle controllers', -> index: jasmine.createSpy('index').andReturn('enterprise fees list') forEnterprise: jasmine.createSpy('forEnterprise').andReturn('enterprise fees for enterprise') - module('order_cycle') + module('admin.order_cycles') inject ($controller) -> ctrl = $controller 'AdminEditOrderCycleCtrl', {$scope: scope, $location: location, OrderCycle: OrderCycle, Enterprise: Enterprise, EnterpriseFee: EnterpriseFee} @@ -323,7 +323,7 @@ describe 'OrderCycle services', -> Enterprise = null beforeEach -> - module 'order_cycle' + module 'admin.order_cycles' inject ($injector, _$httpBackend_)-> Enterprise = $injector.get('Enterprise') $httpBackend = _$httpBackend_ @@ -389,7 +389,7 @@ describe 'OrderCycle services', -> EnterpriseFee = null beforeEach -> - module 'order_cycle' + module 'admin.order_cycles' inject ($injector, _$httpBackend_)-> EnterpriseFee = $injector.get('EnterpriseFee') $httpBackend = _$httpBackend_ @@ -431,7 +431,7 @@ describe 'OrderCycle services', -> beforeEach -> $window = {navigator: {userAgent: 'foo'}} - module 'order_cycle', ($provide)-> + module 'admin.order_cycles', ($provide)-> $provide.value('$window', $window) null From 8b616e7d17ebf389ea2d706620758b978542a869 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 10:21:19 +1100 Subject: [PATCH 062/681] Add unit spec for order cycles simple create controller --- .../controllers/simple_edit.js.coffee | 10 ++-- .../controllers/simple_edit.js.coffee | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee index 728b27fb4a..e2a60e424e 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee @@ -1,19 +1,21 @@ angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl", ($scope, OrderCycle, Enterprise, EnterpriseFee) -> $scope.enterprises = Enterprise.index (enterprises) => + $scope.init(enterprises) + $scope.enterprise_fees = EnterpriseFee.index() + $scope.order_cycle = OrderCycle.order_cycle + + $scope.init = (enterprises) -> enterprise = enterprises[Object.keys(enterprises)[0]] OrderCycle.addSupplier enterprise.id OrderCycle.addDistributor enterprise.id $scope.outgoing_exchange = OrderCycle.order_cycle.outgoing_exchanges[0] + # All variants start as checked OrderCycle.setExchangeVariants(OrderCycle.order_cycle.incoming_exchanges[0], Enterprise.suppliedVariants(enterprise.id), true) OrderCycle.order_cycle.coordinator_id = enterprise.id - $scope.enterprise_fees = EnterpriseFee.index() - - $scope.order_cycle = OrderCycle.order_cycle - $scope.loaded = -> Enterprise.loaded && EnterpriseFee.loaded diff --git a/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee b/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee new file mode 100644 index 0000000000..bf2498e234 --- /dev/null +++ b/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee @@ -0,0 +1,49 @@ +describe "AdminSimpleCreateOrderCycleCtrl", -> + ctrl = null + scope = {} + OrderCycle = {} + Enterprise = {} + EnterpriseFee = {} + incoming_exchange = {} + outgoing_exchange = {} + + beforeEach -> + scope = {} + OrderCycle = + order_cycle: + incoming_exchanges: [incoming_exchange] + outgoing_exchanges: [outgoing_exchange] + addSupplier: jasmine.createSpy() + addDistributor: jasmine.createSpy() + setExchangeVariants: jasmine.createSpy() + Enterprise = + index: jasmine.createSpy() + suppliedVariants: jasmine.createSpy().andReturn('supplied variants') + EnterpriseFee = + index: jasmine.createSpy() + + module('admin.order_cycles') + inject ($controller) -> + ctrl = $controller 'AdminSimpleCreateOrderCycleCtrl', {$scope: scope, OrderCycle: OrderCycle, Enterprise: Enterprise, EnterpriseFee: EnterpriseFee} + + describe "initialisation", -> + enterprise = {id: 123} + enterprises = {123: enterprise} + + beforeEach -> + scope.init(enterprises) + + it "sets up an incoming and outgoing exchange", -> + expect(OrderCycle.addSupplier).toHaveBeenCalledWith(enterprise.id) + expect(OrderCycle.addDistributor).toHaveBeenCalledWith(enterprise.id) + expect(scope.outgoing_exchange).toEqual outgoing_exchange + + it "selects all variants", -> + expect(Enterprise.suppliedVariants). + toHaveBeenCalledWith(enterprise.id) + + expect(OrderCycle.setExchangeVariants). + toHaveBeenCalledWith(incoming_exchange, 'supplied variants', true) + + it "sets the coordinator", -> + expect(OrderCycle.order_cycle.coordinator_id).toEqual enterprise.id \ No newline at end of file From 56ebe688dc74b4454b3ad441feaf200b996284da Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 10:22:03 +1100 Subject: [PATCH 063/681] Fix file naming --- .../{simple_edit.js.coffee => simple_create.js.coffee} | 0 .../{simple_edit.js.coffee => simple_create.js.coffee} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/admin/order_cycles/controllers/{simple_edit.js.coffee => simple_create.js.coffee} (100%) rename spec/javascripts/unit/admin/order_cycles/controllers/{simple_edit.js.coffee => simple_create.js.coffee} (100%) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee similarity index 100% rename from app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee rename to app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee diff --git a/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee b/spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee similarity index 100% rename from spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee rename to spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee From 2e0d5eb8295e9e7afe2c18ee96473032d0265e71 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 10:27:53 +1100 Subject: [PATCH 064/681] Adding set_sells action to enterprises --- app/models/spree/ability_decorator.rb | 2 +- config/routes.rb | 4 ++++ spec/models/spree/ability_spec.rb | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index a5d5f8be33..e3c90c7aa3 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -50,7 +50,7 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty can [:admin, :index, :create], Enterprise - can [:read, :edit, :update, :bulk_update], Enterprise do |enterprise| + can [:read, :edit, :update, :bulk_update, :set_sells], Enterprise do |enterprise| user.enterprises.include? enterprise end diff --git a/config/routes.rb b/config/routes.rb index ca4a6b59ff..53169df888 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,6 +48,10 @@ Openfoodnetwork::Application.routes.draw do post :bulk_update, as: :bulk_update end + member do + post :set_sells + end + resources :producer_properties do post :update_positions, on: :collection end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 0274099aa7..dd42b73e01 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -280,11 +280,11 @@ module Spree end it 'should have the ability to read and edit enterprises that I manage' do - should have_ability([:read, :edit, :update, :bulk_update], for: s1) + should have_ability([:read, :edit, :update, :bulk_update, :set_sells], for: s1) end it 'should not have the ability to read and edit enterprises that I do not manage' do - should_not have_ability([:read, :edit, :update, :bulk_update], for: s2) + should_not have_ability([:read, :edit, :update, :bulk_update, :set_sells], for: s2) end it 'should have the ability administrate and create enterpises' do From 842e239893112a01c74c6b317b2406710d0077f6 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 10:56:30 +1100 Subject: [PATCH 065/681] Make selector containers anchors --- app/views/spree/admin/overview/welcome.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index 6aef4ee027..bc9e0d5b82 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -13,7 +13,7 @@ .options.sixteen.columns.alpha - if @enterprise.is_primary_producer .basic_producer.option.one-third.column.alpha - .selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } + %a.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } .top %h2 Basic Producer .plain_text Supply only @@ -24,7 +24,7 @@ You can also add your products, allowing customers to see your product range and allowing you to act as a supplier to other shopfronts. .producer_shop.option.one-third.column - .selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } + %a.selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } .top %h2 Producer with Shop .plain_text Sell your products through an OFN shopfront @@ -35,7 +35,7 @@ After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. .full_hub.option.one-third.column.omega.disabled - .selector + %a.selector .top.center %h2 Full Hub .plain_text Sell other producers' products through an OFN shopfront @@ -47,7 +47,7 @@ - else .shop_profile.option.one-third.column.alpha - .selector{ ng: { class: "{selected: sells=='none'}" } } + %a.selector{ ng: { class: "{selected: sells=='none'}" } } .top.center %h2 Shop Profile .plain_text Get a listing From 2f045203ab4f2b107586e9da4c581fdfa64ae05d Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 10:56:40 +1100 Subject: [PATCH 066/681] Styling welcome page WIP --- app/assets/stylesheets/admin/welcome.css.sass | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass index 11a15a6314..fffac4570e 100644 --- a/app/assets/stylesheets/admin/welcome.css.sass +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -24,7 +24,10 @@ .options .option - .selector + a.selector + opacity: 1 + display: block + cursor: pointer border: 3px solid black border-radius: 12px 12px 0px 0px text-align: center @@ -39,7 +42,7 @@ padding: 8px 0px &:hover - cursor: 'pointer' + opacity: 0.8 &.selected color: #ffffff From f7c1340f9945585c49dbf986b1589cc5c5e7e2c6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 11:08:27 +1100 Subject: [PATCH 067/681] Admin can view an order cycle in the simple edit form --- .../controllers/simple_edit.js.coffee | 14 +++++++ .../services/order_cycle.js.coffee | 4 +- app/views/admin/order_cycles/edit.html.haml | 9 ++++- spec/features/admin/order_cycles_spec.rb | 31 +++++++++++++++ .../controllers/simple_edit.js.coffee | 38 +++++++++++++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee create mode 100644 spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee new file mode 100644 index 0000000000..bceef1b0ef --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee @@ -0,0 +1,14 @@ +angular.module('admin.order_cycles').controller "AdminSimpleEditOrderCycleCtrl", ($scope, $location, OrderCycle, Enterprise, EnterpriseFee) -> + $scope.enterprises = Enterprise.index() + $scope.enterprise_fees = EnterpriseFee.index() + $scope.order_cycle = OrderCycle.load $scope.orderCycleId(), (order_cycle) => + $scope.init() + + $scope.orderCycleId = -> + $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1] + + $scope.init = -> + $scope.outgoing_exchange = OrderCycle.order_cycle.outgoing_exchanges[0] + + $scope.enterpriseFeesForEnterprise = (enterprise_id) -> + EnterpriseFee.forEnterprise(parseInt(enterprise_id)) diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index b53c91257e..687a0164fa 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -84,7 +84,7 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) for exchange in this.order_cycle.outgoing_exchanges exchange.variants[variant_id] = false - load: (order_cycle_id) -> + load: (order_cycle_id, callback=null) -> service = this OrderCycle.get {order_cycle_id: order_cycle_id}, (oc) -> angular.extend(service.order_cycle, oc) @@ -104,6 +104,8 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) delete(service.order_cycle.exchanges) service.loaded = true + (callback || angular.noop)(service.order_cycle) + this.order_cycle create: -> diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index b22f0cf51d..ff40cfbbbc 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -1,4 +1,9 @@ %h1 Edit Order Cycle -= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => 'AdminEditOrderCycleCtrl', 'ng-submit' => 'submit()'} do |f| - = render 'form', :f => f +- ng_controller = order_cycles_simple_view ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' + += form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit()'} do |f| + - if order_cycles_simple_view + = render 'simple_form', f: f + - else + = render 'form', f: f diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 153f34f4a7..3cff362f56 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -648,6 +648,37 @@ feature %q{ ex.pickup_time.should == 'pickup time' ex.pickup_instructions.should == 'pickup instructions' end + + scenario "editing an order cycle" do + # Given an order cycle + fee = create(:enterprise_fee, name: 'my fee', enterprise: enterprise) + oc = create(:simple_order_cycle, suppliers: [enterprise], coordinator: enterprise, distributors: [enterprise], variants: [p1.master], coordinator_fees: [fee]) + + # And the order cycle has a pickup time and pickup instructions + ex = oc.exchanges.outgoing.first + ex.update_attributes! pickup_time: 'pickup time', pickup_instructions: 'pickup instructions' + + # When I edit it + login_to_admin_section + click_link 'Order Cycles' + click_link oc.name + wait_until { page.find('#order_cycle_name').value.present? } + + # Then I should see the basic settings + page.should have_field 'order_cycle_name', with: oc.name + page.should have_field 'order_cycle_orders_open_at', with: oc.orders_open_at.to_s + page.should have_field 'order_cycle_orders_close_at', with: oc.orders_close_at.to_s + page.should have_field 'order_cycle_outgoing_exchange_0_pickup_time', with: 'pickup time' + page.should have_field 'order_cycle_outgoing_exchange_0_pickup_instructions', with: 'pickup instructions' + + # And I should see the products + page.should have_checked_field "order_cycle_incoming_exchange_0_variants_#{p1.master.id}" + page.should have_unchecked_field "order_cycle_incoming_exchange_0_variants_#{p2.master.id}" + page.should have_unchecked_field "order_cycle_incoming_exchange_0_variants_#{v.id}" + + # And I should see the coordinator fees + page.should have_select 'order_cycle_coordinator_fee_0_id', selected: 'my fee' + end end diff --git a/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee b/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee new file mode 100644 index 0000000000..0e3275d886 --- /dev/null +++ b/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee @@ -0,0 +1,38 @@ +describe "AdminSimpleEditOrderCycleCtrl", -> + ctrl = null + scope = {} + location = {} + OrderCycle = {} + Enterprise = {} + EnterpriseFee = {} + incoming_exchange = {} + outgoing_exchange = {} + + beforeEach -> + scope = {} + location = + absUrl: -> + 'example.com/admin/order_cycles/27/edit' + OrderCycle = + order_cycle: + incoming_exchanges: [incoming_exchange] + outgoing_exchanges: [outgoing_exchange] + load: jasmine.createSpy() + Enterprise = + index: jasmine.createSpy() + EnterpriseFee = + index: jasmine.createSpy() + + module('admin.order_cycles') + inject ($controller) -> + ctrl = $controller 'AdminSimpleEditOrderCycleCtrl', {$scope: scope, $location: location, OrderCycle: OrderCycle, Enterprise: Enterprise, EnterpriseFee: EnterpriseFee} + + describe "initialisation", -> + enterprise = {id: 123} + enterprises = {123: enterprise} + + beforeEach -> + scope.init(enterprises) + + it "sets the outgoing exchange", -> + expect(scope.outgoing_exchange).toEqual outgoing_exchange From ab76c66b6837005ee0858772bc76fb5e2955b3a6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 11:38:15 +1100 Subject: [PATCH 068/681] Admin can update order cycles with simple interface --- .../controllers/simple_edit.js.coffee | 21 ++++++- spec/features/admin/order_cycles_spec.rb | 58 ++++++++++++++++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee index bceef1b0ef..512bc0460b 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee @@ -1,14 +1,31 @@ angular.module('admin.order_cycles').controller "AdminSimpleEditOrderCycleCtrl", ($scope, $location, OrderCycle, Enterprise, EnterpriseFee) -> + $scope.orderCycleId = -> + $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1] + $scope.enterprises = Enterprise.index() $scope.enterprise_fees = EnterpriseFee.index() $scope.order_cycle = OrderCycle.load $scope.orderCycleId(), (order_cycle) => $scope.init() - $scope.orderCycleId = -> - $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1] + $scope.loaded = -> + Enterprise.loaded && EnterpriseFee.loaded && OrderCycle.loaded $scope.init = -> $scope.outgoing_exchange = OrderCycle.order_cycle.outgoing_exchanges[0] $scope.enterpriseFeesForEnterprise = (enterprise_id) -> EnterpriseFee.forEnterprise(parseInt(enterprise_id)) + + $scope.removeDistributionOfVariant = angular.noop + + $scope.addCoordinatorFee = ($event) -> + $event.preventDefault() + OrderCycle.addCoordinatorFee() + + $scope.removeCoordinatorFee = ($event, index) -> + $event.preventDefault() + OrderCycle.removeCoordinatorFee(index) + + $scope.submit = -> + OrderCycle.mirrorIncomingToOutgoingProducts() + OrderCycle.update() diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 3cff362f56..56f3b62916 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -650,11 +650,9 @@ feature %q{ end scenario "editing an order cycle" do - # Given an order cycle + # Given an order cycle with pickup time and instructions fee = create(:enterprise_fee, name: 'my fee', enterprise: enterprise) oc = create(:simple_order_cycle, suppliers: [enterprise], coordinator: enterprise, distributors: [enterprise], variants: [p1.master], coordinator_fees: [fee]) - - # And the order cycle has a pickup time and pickup instructions ex = oc.exchanges.outgoing.first ex.update_attributes! pickup_time: 'pickup time', pickup_instructions: 'pickup instructions' @@ -679,6 +677,60 @@ feature %q{ # And I should see the coordinator fees page.should have_select 'order_cycle_coordinator_fee_0_id', selected: 'my fee' end + + scenario "updating an order cycle" do + # Given an order cycle with pickup time and instructions + fee1 = create(:enterprise_fee, name: 'my fee', enterprise: enterprise) + fee2 = create(:enterprise_fee, name: 'that fee', enterprise: enterprise) + oc = create(:simple_order_cycle, suppliers: [enterprise], coordinator: enterprise, distributors: [enterprise], variants: [p1.master], coordinator_fees: [fee1]) + ex = oc.exchanges.outgoing.first + ex.update_attributes! pickup_time: 'pickup time', pickup_instructions: 'pickup instructions' + + # When I edit it + login_to_admin_section + visit edit_admin_order_cycle_path oc + wait_until { page.find('#order_cycle_name').value.present? } + + # And I fill in the basic fields + fill_in 'order_cycle_name', with: 'Plums & Avos' + fill_in 'order_cycle_orders_open_at', with: '2014-10-17 06:00:00' + fill_in 'order_cycle_orders_close_at', with: '2014-10-24 17:00:00' + fill_in 'order_cycle_outgoing_exchange_0_pickup_time', with: 'xy' + fill_in 'order_cycle_outgoing_exchange_0_pickup_instructions', with: 'zzy' + + # And I make some product selections + uncheck "order_cycle_incoming_exchange_0_variants_#{p1.master.id}" + check "order_cycle_incoming_exchange_0_variants_#{p2.master.id}" + check "order_cycle_incoming_exchange_0_variants_#{v.id}" + uncheck "order_cycle_incoming_exchange_0_variants_#{v.id}" + + # And I select some fees and update + click_link 'order_cycle_coordinator_fee_0_remove' + page.should_not have_select 'order_cycle_coordinator_fee_0_id' + click_button 'Add coordinator fee' + select 'that fee', from: 'order_cycle_coordinator_fee_0_id' + + click_button 'Update' + + # Then my order cycle should have been updated + page.should have_content 'Your order cycle has been updated.' + page.should have_selector 'a', text: 'Plums & Avos' + page.should have_selector "input[value='2014-10-17 06:00:00 +1100']" + page.should have_selector "input[value='2014-10-24 17:00:00 +1100']" + + # And it should have a variant selected + oc = OrderCycle.last + oc.exchanges.incoming.first.variants.should == [p2.master] + oc.exchanges.outgoing.first.variants.should == [p2.master] + + # And it should have the fee + oc.coordinator_fees.should == [fee2] + + # And my pickup time and instructions should have been saved + ex = oc.exchanges.outgoing.first + ex.pickup_time.should == 'xy' + ex.pickup_instructions.should == 'zzy' + end end From 5a837226a460c71fa8233a21724683f92143b899 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 11:38:51 +1100 Subject: [PATCH 069/681] Route set_sells through put rather than post --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 53169df888..62f55065fc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -49,7 +49,7 @@ Openfoodnetwork::Application.routes.draw do end member do - post :set_sells + put :set_sells end resources :producer_properties do From b11c291df1b2971ee5dcf8fbab1616e3948f57a5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 11:39:33 +1100 Subject: [PATCH 070/681] Add set_sells controller action for enterprises --- .../admin/enterprises_controller.rb | 16 +++++ .../admin/enterprises_controller_spec.rb | 70 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 7d38e408d6..8eacba5a53 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -16,6 +16,22 @@ module Admin @collection = order_cycle_permitted_enterprises end + def set_sells + enterprise = Enterprise.find(params[:id]) + attributes = { sells: params[:sells] } + attributes[:shop_trial_start_date] = Time.now if params[:sells] == "own" + + if %w(none own).include?(params[:sells]) + if params[:sells] == 'own' && enterprise.shop_trial_start_date + flash[:error] = "You've already started your trial!" + elsif enterprise.update_attributes(attributes) + flash[:success] = "Congratulations! Registration for #{enterprise.name} is complete!" + end + else + flash[:error] = "Unauthorised" + end + redirect_to admin_path + end def bulk_update @enterprise_set = EnterpriseSet.new(params[:enterprise_set]) diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 4120f163e2..cecb700840 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -110,6 +110,76 @@ module Admin end end + describe "setting 'sells' on an enterprise" do + let(:enterprise) { create(:enterprise, sells: 'none') } + + before do + controller.stub spree_current_user: user + end + + context "as a normal user" do + it "does not allow 'sells' to be set" do + spree_post :set_sells, { id: enterprise.id, sells: 'none' } + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "as a manager" do + before do + enterprise.enterprise_roles.build(user: user).save + end + + context "allows setting 'sells' to 'none'" do + it "is allowed" do + spree_post :set_sells, { id: enterprise.id, sells: 'own' } + expect(response).to redirect_to spree.admin_path + expect(flash[:success]).to eq "Congratulations! Registration for #{enterprise.name} is complete!" + end + end + + context "setting 'sells' to 'own'" do + it "is disallowed if a trial already been started" do + enterprise.sells = 'own' + enterprise.shop_trial_start_date = Date.today.to_time + enterprise.save! + spree_post :set_sells, { id: enterprise.id, sells: 'own' } + expect(response).to redirect_to spree.admin_path + expect(flash[:error]).to eq "You've already started your trial!" + expect(enterprise.reload.sells).to eq 'own' + expect(enterprise.reload.shop_trial_start_date).to eq Date.today.to_time + end + + context "if a trial has not already been started" do + it "is allowed" do + spree_post :set_sells, { id: enterprise.id, sells: 'own' } + expect(response).to redirect_to spree.admin_path + expect(flash[:success]).to eq "Congratulations! Registration for #{enterprise.name} is complete!" + expect(enterprise.reload.sells).to eq 'own' + expect(enterprise.reload.shop_trial_start_date).to be > Time.now-(1.minute) + end + end + end + + context "setting 'sells' to any" do + it "is not allowed" do + spree_post :set_sells, { id: enterprise.id, sells: 'any' } + expect(response).to redirect_to spree.admin_path + expect(flash[:error]).to eq "Unauthorised" + expect(enterprise.reload.sells).to eq 'none' + end + end + + context "settiing 'sells' to 'unspecified'" do + it "is not allowed" do + spree_post :set_sells, { id: enterprise.id, sells: 'unspecified' } + expect(response).to redirect_to spree.admin_path + expect(flash[:error]).to eq "Unauthorised" + expect(enterprise.reload.sells).to eq 'none' + end + end + end + end + describe "bulk updating enterprises" do let!(:original_owner) do user = create_enterprise_user From 7d9c5f9e2c869205af50f9e2057475ccbf6bbaae Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 11:40:11 +1100 Subject: [PATCH 071/681] Adding form elements for submission of sells preferences on welcome page --- .../spree/admin/overview/welcome.html.haml | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index 6aef4ee027..05bbcd845b 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -10,51 +10,54 @@ %br/ the Open Food Network - .options.sixteen.columns.alpha - - if @enterprise.is_primary_producer - .basic_producer.option.one-third.column.alpha - .selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } - .top - %h2 Basic Producer - .plain_text Supply only - .bottom - %h5 ALWAYS FREE - .description - You want to use OFN as a place for people to find and contact you. - You can also add your products, allowing customers to see your product range and allowing you to act as a supplier to other shopfronts. + %form{ name: :sells, method: :put, action: main_app.set_sells_admin_enterprise_path(@enterprise) } + %input{ type: "hidden", id: "sells", name: "sells", ng: { value: "sells" } } + .options.sixteen.columns.alpha + - if @enterprise.is_primary_producer + .basic_producer.option.one-third.column.alpha + .selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } + .top + %h2 Basic Producer + .plain_text Supply only + .bottom + %h5 ALWAYS FREE + .description + You want to use OFN as a place for people to find and contact you. + You can also add your products, allowing customers to see your product range and allowing you to act as a supplier to other shopfronts. - .producer_shop.option.one-third.column - .selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } - .top - %h2 Producer with Shop - .plain_text Sell your products through an OFN shopfront - .bottom - %h5 30 DAY TRIAL - .description - Test out having your own shopfront with full access to all Shopfront features for 30 days. - After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. + .producer_shop.option.one-third.column + .selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } + .top + %h2 Producer with Shop + .plain_text Sell your products through an OFN shopfront + .bottom + %h5 30 DAY TRIAL + .description + Test out having your own shopfront with full access to all Shopfront features for 30 days. + After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. - .full_hub.option.one-third.column.omega.disabled - .selector - .top.center - %h2 Full Hub - .plain_text Sell other producers' products through an OFN shopfront - .bottom.center - %h5 COMING SOON - .description - You want to offer other producers' products for sale as well as your own, establish and coodinate trading relationships with other enterprises and - A full hub subscription gives you access to our complete suite of features for managing your food enterprise. + .full_hub.option.one-third.column.omega.disabled + .selector + .top.center + %h2 Full Hub + .plain_text Sell other producers' products through an OFN shopfront + .bottom.center + %h5 COMING SOON + .description + You want to offer other producers' products for sale as well as your own, establish and coodinate trading relationships with other enterprises and + A full hub subscription gives you access to our complete suite of features for managing your food enterprise. - - else - .shop_profile.option.one-third.column.alpha - .selector{ ng: { class: "{selected: sells=='none'}" } } - .top.center - %h2 Shop Profile - .plain_text Get a listing - .bottom.center - %h5 ALWAYS FREE - .description - You want to use OFN as a place for people to find and contact you. + - else + .shop_profile.option.one-third.column.alpha + .selector{ ng: { class: "{selected: sells=='none'}" } } + .top.center + %h2 Shop Profile + .plain_text Get a listing + .bottom.center + %h5 ALWAYS FREE + .description + You want to use OFN as a place for people to find and contact you. - // Coming soon - Full Hub - // Email us if you want this option \ No newline at end of file + // Coming soon - Full Hub + // Email us if you want this option + %input{ type: 'submit' } \ No newline at end of file From f5aae7d2143d1aa5b22a7ca72fffe15b020235f9 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 14:23:04 +1100 Subject: [PATCH 072/681] Pretty stripy background tile --- app/assets/images/stripe.png | Bin 0 -> 1091 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/assets/images/stripe.png diff --git a/app/assets/images/stripe.png b/app/assets/images/stripe.png new file mode 100644 index 0000000000000000000000000000000000000000..8ce0d7e0db6411ae6f52c19fe3ccbdd585f77462 GIT binary patch literal 1091 zcmaJ=O-K|`93P6U&?KoSjLc~yF>-ficU^a9+*NdE)s04UU1|><9A}<7=*$~up6%?0 zAVmiQA^JoM45W;LVxulal!p$Ig&>i15{V970)?Uvus5r#9oh!oy!U_nf4~3N?~Syz zHWw8fC?E);DB8lu@V*1D;vL)Y|JcR4A-t8KNCI`hZj=-aKs3m(3y@J&>;W+#%Bg`F z5G07Ll}bE;62b{if~rfjeO#8RV>Ceo>n&ZB`T!!kK#!t@sISjoQluh>s8c?H5%e(V zRa(*p=t#H5rF5Uf%2a(F8MHV|paLY4mfEkGoE4(7yc|B;*EB_DT~J?$T1zS+w2@(G z0Mh5GaY~H4n)I_Sx39*}`m0C}wrM6ndl)Cfay~D|c*ymS!qE)5n~U*H>#?vEqIwb1 zIhsx;ldh!K1&tos&9W@bcxaEui9MWVN<*UM)Xef60}o8eP;{g~jkFoXE;xun6wY+5 z1XbUl)y(xa;fB$csMBs2W0#b36omh~s_KTfiDFqMwm)oIq zE^Gi1K_d=ff3Aydy%0gO7wTjhiO>7`9))s`SVq-)8K*O3hu()4%Q%(3` zWTJdS-1>O>Qv3ET?Zp>*rds;b(GZ{Mnx9x$U3~)k?oeM2e>*~D25Y}Ro18DL>z*xn zH#W=O7-?}-EI%l`H#FIKt*ZX^)9J$FKOU7^k30D{`vY^q51-PzelDDawflmv#y&5O zI*Rm6*|>iXT5MVxP0d^H=Zu{Ntpkqa?*8dvhtoOLIg`0Mu_V2=PL@t3hD+W~-(2pv qJmr8DiSxIv! Date: Thu, 23 Oct 2014 14:23:23 +1100 Subject: [PATCH 073/681] Tweak image add gradient blur and slightly less dark --- app/assets/images/home/tagline-bg.jpg | Bin 166929 -> 166404 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/images/home/tagline-bg.jpg b/app/assets/images/home/tagline-bg.jpg index 161764ef210aea627901afc35052ea3f5fad85ec..68366a95695dcd0716474fbb6832f6439b52b5b5 100644 GIT binary patch literal 166404 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

N9=Ad|of+Wr7G2 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 literal 166929 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 BZyx{v From 694724d17782073abcd5efcbb57af3add0c7494c Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 14:23:35 +1100 Subject: [PATCH 074/681] Refactoring and styling Welcome screen dashboard admin WIP --- app/assets/stylesheets/admin/welcome.css.sass | 180 ++++++++++++------ .../spree/admin/overview/welcome.html.haml | 71 ++++--- 2 files changed, 156 insertions(+), 95 deletions(-) diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass index fffac4570e..2494399c27 100644 --- a/app/assets/stylesheets/admin/welcome.css.sass +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -1,65 +1,133 @@ +@import ../darkswarm/branding +@import ../darkswarm/mixins + #welcome_page - color: #000000 - h1, h2, h3, h4, h5, h6 - color: #000000 - - .big - font-weight: bold - font-size: 36px - - .plain_text - font-size: 16px - - #welcome + header text-align: center - margin: 50px 0px - h1 - margin-bottom: 10px + padding: 4em 2em + @include fullbg + background-image: url('/assets/home/tagline-bg.jpg') + background-repeat: no-repeat + background-position: center center + margin-bottom: 2em + p + text-transform: uppercase + font-weight: 300 + &, & * + color: white + section + margin: 2em 0 0 0 + &, & * + color: #5498da - #next_steps + .description + background-color: #eff5fc + margin-top: -2em + margin-bottom: 2em + padding: 4em 2em 2em 1em + + a.selector + position: relative + border: 2px solid black text-align: center - margin-bottom: 50px - h1 - margin-bottom: 10px + width: 100% + cursor: pointer + &, & * + color: white + &:after, &:before + top: 100% + left: 50% + border: solid transparent + content: " " + height: 0 + width: 0 + position: absolute + pointer-events: none + &:after + border-color: rgba(136, 183, 213, 0) + border-top-color: #5498da + border-width: 12px + margin-left: -12px + &:hover + &:after + border-top-color: #9fc820 + &:before + border-color: rgba(84, 152, 218, 0) + border-top-color: black + border-width: 15px + margin-left: -15px + .bottom + background-image: url('/assets/stripe.png') + margin-top: 1em + margin-left: -15px + margin-right: -15px + padding: 5px + text-transform: uppercase - .options - .option - a.selector - opacity: 1 - display: block - cursor: pointer - border: 3px solid black - border-radius: 12px 12px 0px 0px - text-align: center - margin-bottom: 20px - .top - min-height: 113px - padding: 15px 8px - h2 - margin-bottom: 10px - .bottom - border-top: 3px solid black - padding: 8px 0px - &:hover - opacity: 0.8 - &.selected - color: #ffffff - background-color: #ff4444 - border-color: #000000 - h1, h2, h3, h4, h5, h6 - color: #ffffff +// #welcome_page +// color: #000000 +// h1, h2, h3, h4, h5, h6 +// color: #000000 - &.disabled - color: #b0b0b0 - .selector - border-color: #b0b0b0 - h1, h2, h3, h4, h5, h6 - color: #b0b0b0 - .bottom - border-top-color: #b0b0b0 +// .big +// font-weight: bold +// font-size: 36px - .description - font-size: 16px - text-align: justify \ No newline at end of file +// .plain_text +// font-size: 16px + +// #welcome +// text-align: center +// margin: 50px 0px +// h1 +// margin-bottom: 10px + +// #next_steps +// text-align: center +// margin-bottom: 50px +// h1 +// margin-bottom: 10px + +// .options +// .option +// a.selector +// opacity: 1 +// display: block +// cursor: pointer +// border: 3px solid black +// border-radius: 12px 12px 0px 0px +// text-align: center +// margin-bottom: 20px +// .top +// min-height: 113px +// padding: 15px 8px +// h2 +// margin-bottom: 10px +// .bottom +// border-top: 3px solid black +// padding: 8px 0px + +// &:hover +// opacity: 0.8 + +// &.selected +// color: #ffffff +// background-color: #ff4444 +// border-color: #000000 +// h1, h2, h3, h4, h5, h6 +// color: #ffffff + +// &.disabled +// color: #b0b0b0 +// .selector +// border-color: #b0b0b0 +// h1, h2, h3, h4, h5, h6 +// color: #b0b0b0 +// .bottom +// border-top-color: #b0b0b0 + +// .description +// font-size: 16px +// text-align: justify \ No newline at end of file diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index bc9e0d5b82..0e6dbafb6e 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -1,58 +1,51 @@ #welcome_page.sixteen.columns.alpha{ ng: { app: "admin.welcome", controller: 'welcomeCtrl' } } - #welcome - .big Welcome to the Open Food Network! - %h4= "You have successfully created a#{" producer" if @enterprise.is_primary_producer} profile" + %header + %h1 Welcome to the Open Food Network! + %p + You have successfully created a + %strong + = "#{"producer " if @enterprise.is_primary_producer}profile" - #next_steps - %h1 Next Steps - .plain_text - Choose how you would like to use - %br/ - the Open Food Network + %section + %h2 Next step + %p Choose one: .options.sixteen.columns.alpha - if @enterprise.is_primary_producer .basic_producer.option.one-third.column.alpha - %a.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } + %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } .top - %h2 Basic Producer - .plain_text Supply only - .bottom - %h5 ALWAYS FREE - .description - You want to use OFN as a place for people to find and contact you. - You can also add your products, allowing customers to see your product range and allowing you to act as a supplier to other shopfronts. + %h3 Producer Profile + %p Connect through OFN + .bottom ALWAYS FREE + %p.description + You want to use Open Food Network as a place for people to find and contact you. .producer_shop.option.one-third.column - %a.selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } + %a.full-width.button.selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } .top - %h2 Producer with Shop - .plain_text Sell your products through an OFN shopfront - .bottom - %h5 30 DAY TRIAL - .description - Test out having your own shopfront with full access to all Shopfront features for 30 days. - After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. + %h3 Sell products + %p As a supplier + .bottom ALWAYS FREE + %p.description + Add your products to Open Food Network, allowing customers to see your product range, and allowing you to act as a supplier to other shopfronts. .full_hub.option.one-third.column.omega.disabled - %a.selector - .top.center - %h2 Full Hub - .plain_text Sell other producers' products through an OFN shopfront - .bottom.center - %h5 COMING SOON - .description - You want to offer other producers' products for sale as well as your own, establish and coodinate trading relationships with other enterprises and - A full hub subscription gives you access to our complete suite of features for managing your food enterprise. + %a.full-width.button.selector + .top + %h3 Sell products + %p Through an OFN shopfront + .bottom 30 DAY TRIAL + %p.description + Test out having your own shopfront with full access to all Shopfront features for 30 days. After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. - else .shop_profile.option.one-third.column.alpha %a.selector{ ng: { class: "{selected: sells=='none'}" } } - .top.center - %h2 Shop Profile - .plain_text Get a listing - .bottom.center - %h5 ALWAYS FREE + .top + %h3 Shop Profile + %p Get a listing + .bottom ALWAYS FREE .description You want to use OFN as a place for people to find and contact you. From 77c0e36ed65c9b8a04b18787995ca363765bb514 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 14:23:50 +1100 Subject: [PATCH 075/681] Adding input validation to welcome page --- .../welcome/controllers/welcome_controller.js.coffee | 10 +++++++++- app/views/spree/admin/overview/welcome.html.haml | 10 +++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee b/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee index f9d43b672e..9883e90937 100644 --- a/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee +++ b/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee @@ -1,3 +1,11 @@ angular.module("admin.welcome") .controller "welcomeCtrl", ($scope) -> - $scope.sells = "unspecified" \ No newline at end of file + $scope.sells = "unspecified" + $scope.submitted = false + + $scope.valid = (form) -> + $scope.submitted = !form.$valid + form.$valid + + $scope.submit = (form) -> + event.preventDefault() unless $scope.valid(form) diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index 05bbcd845b..6fdfa40a40 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -10,8 +10,9 @@ %br/ the Open Food Network - %form{ name: :sells, method: :put, action: main_app.set_sells_admin_enterprise_path(@enterprise) } - %input{ type: "hidden", id: "sells", name: "sells", ng: { value: "sells" } } + = form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), novalidate: true do |enterprise_form| + -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation + %input{ hidden: "true", id: "sells", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } .options.sixteen.columns.alpha - if @enterprise.is_primary_producer .basic_producer.option.one-third.column.alpha @@ -60,4 +61,7 @@ // Coming soon - Full Hub // Email us if you want this option - %input{ type: 'submit' } \ No newline at end of file + .sixteen.columns.alpha + %span.error{ ng: { show: "(enterprise.sells.$error.pattern || enterprise.sells.$error.pattern) && submitted" } } + Please select an option + %input{ type: 'submit', ng: { click: "submit(enterprise)" } } \ No newline at end of file From 204c7d96902a342833ef8f26e09487676e0cd74b Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 14:26:52 +1100 Subject: [PATCH 076/681] Selected state push --- app/assets/stylesheets/admin/welcome.css.sass | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass index 2494399c27..fc062c8297 100644 --- a/app/assets/stylesheets/admin/welcome.css.sass +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -63,6 +63,10 @@ margin-right: -15px padding: 5px text-transform: uppercase + &.selected + background-color: black + &:after, &:hover &:after + border-top-color: black From d6947b119ff0a303d2a59db1da1738b5b004d972 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 14:28:53 +1100 Subject: [PATCH 077/681] Welcome markup make alternative markup consistent --- app/views/spree/admin/overview/welcome.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index 0e6dbafb6e..e3945de5f8 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -41,7 +41,7 @@ - else .shop_profile.option.one-third.column.alpha - %a.selector{ ng: { class: "{selected: sells=='none'}" } } + %a.full-width.button.selector{ ng: { class: "{selected: sells=='none'}" } } .top %h3 Shop Profile %p Get a listing From 04af954432b37c7fb6f109f50a764eb85fafb63f Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 14:33:19 +1100 Subject: [PATCH 078/681] Tweak language --- app/views/spree/admin/overview/welcome.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index e3945de5f8..fd28809a0a 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -8,7 +8,7 @@ %section %h2 Next step - %p Choose one: + %p Choose your starting point: .options.sixteen.columns.alpha - if @enterprise.is_primary_producer From 67c82e81dec443e8781f1be2e2aa7eb43d6a610b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 14:41:00 +1100 Subject: [PATCH 079/681] Fixing form on welcome page --- app/views/spree/admin/overview/welcome.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index ec29b83045..2fd1556395 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -10,7 +10,7 @@ %h2 Next step %p Choose one: - = form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), novalidate: true do |enterprise_form| + = form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), html: { name: "enterprise", novalidate: true } do |enterprise_form| -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation %input{ hidden: "true", id: "sells", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } .options.sixteen.columns.alpha From e9a6c9d0ce426d32905fba1ff10187d656571986 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 14:50:56 +1100 Subject: [PATCH 080/681] Adding header to welcome page --- app/views/spree/layouts/bare_admin.html.haml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/views/spree/layouts/bare_admin.html.haml b/app/views/spree/layouts/bare_admin.html.haml index 8440252350..2f24cb2ed2 100644 --- a/app/views/spree/layouts/bare_admin.html.haml +++ b/app/views/spree/layouts/bare_admin.html.haml @@ -15,6 +15,12 @@ = t(:loading) \... = render :partial => 'spree/admin/shared/alert', :collection => session[:alerts] + + %header#header{"data-hook" => ""} + .container + %figure.columns.five{"data-hook" => "logo-wrapper"}= link_to image_tag(Spree::Config[:admin_interface_logo], :id => 'logo'), spree.admin_path + %nav.columns.eleven{"data-hook" => "admin_login_navigation_bar"} + .container .row #content{"data-hook" => ""} From ce346d3bcaf354b53ef6e77751998a7730c63f8a Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 15:17:17 +1100 Subject: [PATCH 081/681] Styling stripes with CSS3, remove the image instead --- app/assets/images/stripe.png | Bin 1091 -> 0 bytes app/assets/stylesheets/admin/welcome.css.sass | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 app/assets/images/stripe.png diff --git a/app/assets/images/stripe.png b/app/assets/images/stripe.png deleted file mode 100644 index 8ce0d7e0db6411ae6f52c19fe3ccbdd585f77462..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1091 zcmaJ=O-K|`93P6U&?KoSjLc~yF>-ficU^a9+*NdE)s04UU1|><9A}<7=*$~up6%?0 zAVmiQA^JoM45W;LVxulal!p$Ig&>i15{V970)?Uvus5r#9oh!oy!U_nf4~3N?~Syz zHWw8fC?E);DB8lu@V*1D;vL)Y|JcR4A-t8KNCI`hZj=-aKs3m(3y@J&>;W+#%Bg`F z5G07Ll}bE;62b{if~rfjeO#8RV>Ceo>n&ZB`T!!kK#!t@sISjoQluh>s8c?H5%e(V zRa(*p=t#H5rF5Uf%2a(F8MHV|paLY4mfEkGoE4(7yc|B;*EB_DT~J?$T1zS+w2@(G z0Mh5GaY~H4n)I_Sx39*}`m0C}wrM6ndl)Cfay~D|c*ymS!qE)5n~U*H>#?vEqIwb1 zIhsx;ldh!K1&tos&9W@bcxaEui9MWVN<*UM)Xef60}o8eP;{g~jkFoXE;xun6wY+5 z1XbUl)y(xa;fB$csMBs2W0#b36omh~s_KTfiDFqMwm)oIq zE^Gi1K_d=ff3Aydy%0gO7wTjhiO>7`9))s`SVq-)8K*O3hu()4%Q%(3` zWTJdS-1>O>Qv3ET?Zp>*rds;b(GZ{Mnx9x$U3~)k?oeM2e>*~D25Y}Ro18DL>z*xn zH#W=O7-?}-EI%l`H#FIKt*ZX^)9J$FKOU7^k30D{`vY^q51-PzelDDawflmv#y&5O zI*Rm6*|>iXT5MVxP0d^H=Zu{Ntpkqa?*8dvhtoOLIg`0Mu_V2=PL@t3hD+W~-(2pv qJmr8DiSxIv! Date: Thu, 23 Oct 2014 15:21:16 +1100 Subject: [PATCH 082/681] Adding login nav to header on bare_admin layout --- .../spree/layouts/bare_admin/add_login_nav.html.haml.deface | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 app/overrides/spree/layouts/bare_admin/add_login_nav.html.haml.deface diff --git a/app/overrides/spree/layouts/bare_admin/add_login_nav.html.haml.deface b/app/overrides/spree/layouts/bare_admin/add_login_nav.html.haml.deface new file mode 100644 index 0000000000..99ef28bc9c --- /dev/null +++ b/app/overrides/spree/layouts/bare_admin/add_login_nav.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_top "[data-hook='admin_login_navigation_bar']" + += render partial: "spree/layouts/admin/login_nav" \ No newline at end of file From 4f2327f05c932c82835cb6381a41bda9f86b2cff Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 15:46:56 +1100 Subject: [PATCH 083/681] More styling for Next button and error message --- app/assets/stylesheets/admin/welcome.css.sass | 15 +++++++++++++++ app/views/spree/admin/overview/welcome.html.haml | 11 ++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass index 1ea3b1a0e5..f9e6a5e212 100644 --- a/app/assets/stylesheets/admin/welcome.css.sass +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -25,6 +25,21 @@ margin-top: -2em margin-bottom: 2em padding: 4em 2em 2em 1em + + .admin-cta + border: 1px solid #5498da + @include border-radius(3px) + text-align: center + padding: 1em + + .error + display: block + color: white + background: red + margin-bottom: 1em + padding: 0.5em + font-style: oblique + a.selector position: relative diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index dc44c3847f..68cd58a748 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -53,6 +53,11 @@ You want to use OFN as a place for people to find and contact you. .sixteen.columns.alpha - %span.error{ ng: { show: "(enterprise.sells.$error.pattern || enterprise.sells.$error.pattern) && submitted" } } - Please select an option - %input{ type: 'submit', ng: { click: "submit(enterprise)" } } \ No newline at end of file + .admin-cta + %span.error{ ng: { show: "(enterprise.sells.$error.pattern || enterprise.sells.$error.pattern) && submitted" } } + Please choose one of the options above. + %input{ type: 'submit', value: 'Next', ng: { click: "submit(enterprise)" } } + + + + From 7faf6e12eacba9e8d10358f72d76168c55db4bf2 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 15:53:16 +1100 Subject: [PATCH 084/681] Clean up simple order cycle interface --- app/views/admin/order_cycles/_simple_form.html.haml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/views/admin/order_cycles/_simple_form.html.haml b/app/views/admin/order_cycles/_simple_form.html.haml index 912631bb37..9bffb9753e 100644 --- a/app/views/admin/order_cycles/_simple_form.html.haml +++ b/app/views/admin/order_cycles/_simple_form.html.haml @@ -2,19 +2,22 @@ .row .alpha.two.columns - = label_tag 'Pickup time' + = label_tag 'Ready for' .six.columns - = text_field_tag 'order_cycle_outgoing_exchange_0_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_time', 'placeholder' => 'Ready for (ie. Date / Time)', 'ng-model' => 'outgoing_exchange.pickup_time' + = text_field_tag 'order_cycle_outgoing_exchange_0_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_time', 'placeholder' => 'Date / time', 'ng-model' => 'outgoing_exchange.pickup_time', 'size' => 30 .two.columns - = label_tag 'Pickup instructions' + = label_tag 'Customer instructions' .six.columns.omega - = text_field_tag 'order_cycle_outgoing_exchange_0_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_instructions', 'placeholder' => 'Pick-up instructions', 'ng-model' => 'outgoing_exchange.pickup_instructions' + = text_field_tag 'order_cycle_outgoing_exchange_0_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_instructions', 'placeholder' => 'Pick-up or delivery notes', 'ng-model' => 'outgoing_exchange.pickup_instructions', 'size' => 30 += label_tag 'Products' %table.exchanges %tbody{ng: {repeat: "exchange in order_cycle.incoming_exchanges"}} %tr.products = render 'exchange_supplied_products_form' +%br/ += label_tag 'Fees' = render 'coordinator_fees', f: f .actions From f09698be47785fd0f02d6a56375b88f404f82e06 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 15:54:52 +1100 Subject: [PATCH 085/681] Select all works on simple order cycles edit interface --- .../admin/order_cycles/controllers/simple_edit.js.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee index 512bc0460b..bcdaa64e91 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee @@ -18,6 +18,12 @@ angular.module('admin.order_cycles').controller "AdminSimpleEditOrderCycleCtrl", $scope.removeDistributionOfVariant = angular.noop + $scope.setExchangeVariants = (exchange, variants, selected) -> + OrderCycle.setExchangeVariants(exchange, variants, selected) + + $scope.suppliedVariants = (enterprise_id) -> + Enterprise.suppliedVariants(enterprise_id) + $scope.addCoordinatorFee = ($event) -> $event.preventDefault() OrderCycle.addCoordinatorFee() From d94ca0174a62e82e31739234bc9a0fa76c9f6ec6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 15:47:46 +1100 Subject: [PATCH 086/681] Registration process sets sells to 'unspecified' --- app/controllers/api/enterprises_controller.rb | 5 +++++ spec/features/consumer/registration_spec.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/enterprises_controller.rb b/app/controllers/api/enterprises_controller.rb index 76320b0750..b1570fb0d1 100644 --- a/app/controllers/api/enterprises_controller.rb +++ b/app/controllers/api/enterprises_controller.rb @@ -3,6 +3,7 @@ module Api before_filter :override_owner, only: [:create, :update] before_filter :check_type, only: :update + before_filter :override_sells, only: [:create, :update] respond_to :json def managed @@ -59,5 +60,9 @@ module Api def check_type params[:enterprise].delete :type unless current_api_user.admin? end + + def override_sells + params[:enterprise][:sells] = 'unspecified' + end end end diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index ea09450467..354a5c37eb 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -51,7 +51,7 @@ feature "Registration", js: true do expect(page).to have_content 'Nice one!' e = Enterprise.find_by_name('My Awesome Enterprise') expect(e.address.address1).to eq "123 Abc Street" - expect(e.sells).to eq "none" + expect(e.sells).to eq "unspecified" expect(e.is_primary_producer).to eq true expect(e.contact).to eq "Saskia Munroe" From 996b2f2604bacc31e346fa85eac9332e8e23e68c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 16:06:20 +1100 Subject: [PATCH 087/681] Adding 'producer_profile_only' flag to enterprises --- ...0141023050324_add_producer_profile_only_to_enterprises.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20141023050324_add_producer_profile_only_to_enterprises.rb diff --git a/db/migrate/20141023050324_add_producer_profile_only_to_enterprises.rb b/db/migrate/20141023050324_add_producer_profile_only_to_enterprises.rb new file mode 100644 index 0000000000..fb73d123f8 --- /dev/null +++ b/db/migrate/20141023050324_add_producer_profile_only_to_enterprises.rb @@ -0,0 +1,5 @@ +class AddProducerProfileOnlyToEnterprises < ActiveRecord::Migration + def change + add_column :enterprises, :producer_profile_only, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 3c1fb143e4..a3245f549e 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 => 20141022050659) do +ActiveRecord::Schema.define(:version => 20141023050324) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -270,6 +270,7 @@ ActiveRecord::Schema.define(:version => 20141022050659) do t.datetime "confirmation_sent_at" t.string "unconfirmed_email" t.datetime "shop_trial_start_date" + t.boolean "producer_profile_only", :default => false end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" From e1cf5ceb57d71ad94d9cca79abfae2cb2fa1aff9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 16:40:58 +1100 Subject: [PATCH 088/681] Can set producer_profile_only using welcome page --- .../controllers/welcome_controller.js.coffee | 1 + .../admin/enterprises_controller.rb | 1 + .../spree/admin/overview/welcome.html.haml | 11 +++++----- .../admin/enterprises_controller_spec.rb | 21 +++++++++++++++++-- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee b/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee index 9883e90937..8db38181c4 100644 --- a/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee +++ b/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee @@ -1,6 +1,7 @@ angular.module("admin.welcome") .controller "welcomeCtrl", ($scope) -> $scope.sells = "unspecified" + $scope.producer_profile_only = true $scope.submitted = false $scope.valid = (form) -> diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 8eacba5a53..8db4101075 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -19,6 +19,7 @@ module Admin def set_sells enterprise = Enterprise.find(params[:id]) attributes = { sells: params[:sells] } + attributes[:producer_profile_only] = !!params[:producer_profile_only] if params[:sells] == 'none' attributes[:shop_trial_start_date] = Time.now if params[:sells] == "own" if %w(none own).include?(params[:sells]) diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index 68cd58a748..b5b031eefb 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -12,11 +12,12 @@ = form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), html: { name: "enterprise", novalidate: true } do |enterprise_form| -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation - %input{ hidden: "true", id: "sells", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } + %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } .options.sixteen.columns.alpha - if @enterprise.is_primary_producer + %input{ type: 'checkbox', hidden: true, name: "producer_profile_only", ng: { required: true, model: 'producer_profile_only', value: "producer_profile_only"} } .basic_producer.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } + %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=true;", class: "{selected: sells=='none' && producer_profile_only==true}" } } .top %h3 Producer Profile %p Connect through OFN @@ -25,7 +26,7 @@ You want to use Open Food Network as a place for people to find and contact you. .producer_shop.option.one-third.column - %a.full-width.button.selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } + %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=false;", class: "{selected: sells=='none' && producer_profile_only==false}" } } .top %h3 Sell products %p As a supplier @@ -34,7 +35,7 @@ Add your products to Open Food Network, allowing customers to see your product range, and allowing you to act as a supplier to other shopfronts. .full_hub.option.one-third.column.omega.disabled - %a.full-width.button.selector + %a.full-width.button.selector{ ng: { click: "sells='own';producer_profile_only=false;", class: "{selected: sells=='own' && producer_profile_only==false}" } } .top %h3 Sell products %p Through an OFN shopfront @@ -49,7 +50,7 @@ %h3 Shop Profile %p Get a listing .bottom ALWAYS FREE - .description + %p.description You want to use OFN as a place for people to find and contact you. .sixteen.columns.alpha diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index cecb700840..60f0d9f736 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -110,7 +110,7 @@ module Admin end end - describe "setting 'sells' on an enterprise" do + describe "set_sells" do let(:enterprise) { create(:enterprise, sells: 'none') } before do @@ -131,9 +131,18 @@ module Admin context "allows setting 'sells' to 'none'" do it "is allowed" do - spree_post :set_sells, { id: enterprise.id, sells: 'own' } + spree_post :set_sells, { id: enterprise.id, sells: 'none' } expect(response).to redirect_to spree.admin_path expect(flash[:success]).to eq "Congratulations! Registration for #{enterprise.name} is complete!" + expect(enterprise.reload.sells).to eq 'none' + end + + context "setting producer_profile_only to true" do + it "is allowed" do + spree_post :set_sells, { id: enterprise.id, sells: 'none', producer_profile_only: true } + expect(response).to redirect_to spree.admin_path + expect(enterprise.reload.producer_profile_only).to eq true + end end end @@ -158,6 +167,14 @@ module Admin expect(enterprise.reload.shop_trial_start_date).to be > Time.now-(1.minute) end end + + context "setting producer_profile_only to true" do + it "is ignored" do + spree_post :set_sells, { id: enterprise.id, sells: 'own', producer_profile_only: true } + expect(response).to redirect_to spree.admin_path + expect(enterprise.reload.producer_profile_only).to be false + end + end end context "setting 'sells' to any" do From aea8ab4c7f52153a047b4de73ff9ed2da8fff58a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 16:32:43 +1100 Subject: [PATCH 089/681] Move #manages_one_enterprise? from User model to Permissions --- app/controllers/spree/admin/overview_controller_decorator.rb | 2 +- app/models/spree/user_decorator.rb | 4 ---- lib/open_food_network/permissions.rb | 4 ++++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index 74535d5dd7..c73073c0fc 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -5,7 +5,7 @@ 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 - if spree_current_user.manages_one_enterprise? + if OpenFoodNetwork::Permissions.new(spree_current_user).manages_one_enterprise? @enterprise = @enterprises.first if @enterprise.sells == "unspecified" render "welcome", layout: "spree/layouts/bare_admin" diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index e8b75f6466..06a413b469 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -19,10 +19,6 @@ Spree.user_class.class_eval do end end - def manages_one_enterprise? - enterprises.length == 1 - end - def send_signup_confirmation Spree::UserMailer.signup_confirmation(self).deliver end diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 3d5df1e804..60c2acbeaf 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -25,6 +25,10 @@ module OpenFoodNetwork managed_and_related_enterprises_with :manage_products end + def manages_one_enterprise? + @user.enterprises.length == 1 + end + private From ee5cd599aa39e289ba14cd939471ad582b7845d1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 16:33:17 +1100 Subject: [PATCH 090/681] Add basic details for the single enterprise dashboard --- app/helpers/enterprises_helper.rb | 7 +++ .../single_enterprise_dashboard.html.haml | 48 ++++++++++++++----- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index 89555d3b4f..a5bf2ca794 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -19,6 +19,13 @@ module EnterprisesHelper enterprises.map(&:name).sort.join(', ') end + def enterprise_type_name(enterprise) + # TODO: When we can distinguish between profiles and producers that supply only (without + # their own store), include it here. + enterprise.sells == 'none' ? 'profile' : 'shopfront' + end + + def enterprise_confirm_delete_message(enterprise) if enterprise.supplied_products.present? "This will also delete the #{pluralize enterprise.supplied_products.count, 'product'} that this enterprise supplies. Are you sure you want to continue?" diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 31cdc72a0f..2ab69e8557 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -1,17 +1,39 @@ --# - if @enterprise.sells == "unconfirmed" --# %h1 Welcome to the Open Food Network +#enterprise + %h1= @enterprise.name + %h2= enterprise_type_name(@enterprise) + + -# We need an action to render the welcome template, that this button can link to + %button Change type + +#message + Message bar + +.row + .alpha.seven.columns + See your profile live on the Open Food Network map. + %br/ + -# Can we pass an anchor here to zoom to our enterprise? + = link_to "#{@enterprise.name} live", main_app.map_path + + .two.columns + + .seven.columns.omega + Change or tweak any of the details of your profile + %br/ + = link_to "Manage #{@enterprise.name}", main_app.edit_admin_enterprise_path(@enterprise) -- if @enterprise.is_primary_producer - // Basic Producer +.row + .alpha.seven.columns + - if can? :admin, Spree::Product + Add and manage products + %br/ + = link_to "Manage products", bulk_edit_admin_products_path - // Producer with Shopfront + .two.columns - // Coming soon - Full Hub - // Email us if you want this option - -- else - // Shop Profile - - // Coming soon - Full Hub - // Email us if you want this option + .seven.columns.omega + - if can? :admin, OrderCycle + Add and manage order cycles + %br/ + = link_to "Manage order cycles", main_app.admin_order_cycles_path From 9bc85318500d372efd10742a756686ba77444e1d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 23 Oct 2014 16:48:16 +1100 Subject: [PATCH 091/681] Add comment --- app/helpers/enterprises_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index a5bf2ca794..91821a3914 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -22,6 +22,7 @@ module EnterprisesHelper def enterprise_type_name(enterprise) # TODO: When we can distinguish between profiles and producers that supply only (without # their own store), include it here. + # profile, supplier only, shopfront enterprise.sells == 'none' ? 'profile' : 'shopfront' end From cf06cab533f444ea47c242abf03cd810396e4203 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 17:23:58 +1100 Subject: [PATCH 092/681] Tweak header to be consistent with other admin pages --- .../overview/single_enterprise_dashboard.html.haml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 2ab69e8557..b24d574c24 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -1,7 +1,12 @@ -#enterprise - %h1= @enterprise.name - %h2= enterprise_type_name(@enterprise) +- content_for :page_title do + %h1 + = @enterprise.name + %span.small + ( + = enterprise_type_name(@enterprise) + ) +- content_for :page_actions do -# We need an action to render the welcome template, that this button can link to %button Change type From 3c839be743a6956a50f734bf226e187d5ccd8f03 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 23 Oct 2014 17:24:56 +1100 Subject: [PATCH 093/681] Tweak styling for error message to be consistent with other form error styling in admin --- app/assets/stylesheets/admin/welcome.css.sass | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass index f9e6a5e212..2b208a578e 100644 --- a/app/assets/stylesheets/admin/welcome.css.sass +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -34,12 +34,12 @@ .error display: block - color: white - background: red + color: #f57e80 + border: 1px solid #f57e80 + background-color: #fde6e7 + @include border-radius(3px) margin-bottom: 1em padding: 0.5em - font-style: oblique - a.selector position: relative From d2efd25e8d76d3eae497903810b6539681395117 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Oct 2014 17:22:25 +1100 Subject: [PATCH 094/681] Pull out type change form into a partial --- .../overview/_change_type_form.html.haml | 48 +++++++++++++++++ .../spree/admin/overview/welcome.html.haml | 53 +------------------ 2 files changed, 49 insertions(+), 52 deletions(-) create mode 100644 app/views/spree/admin/overview/_change_type_form.html.haml diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml new file mode 100644 index 0000000000..041c0b15e2 --- /dev/null +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -0,0 +1,48 @@ += form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), html: { name: "enterprise", novalidate: true } do |enterprise_form| + -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation + %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } + .options.sixteen.columns.alpha + - if @enterprise.is_primary_producer + %input{ type: 'checkbox', hidden: true, name: "producer_profile_only", ng: { required: true, model: 'producer_profile_only', value: "producer_profile_only"} } + .basic_producer.option.one-third.column.alpha + %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=true;", class: "{selected: sells=='none' && producer_profile_only==true}" } } + .top + %h3 Producer Profile + %p Connect through OFN + .bottom ALWAYS FREE + %p.description + You want to use Open Food Network as a place for people to find and contact you. + + .producer_shop.option.one-third.column + %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=false;", class: "{selected: sells=='none' && producer_profile_only==false}" } } + .top + %h3 Sell products + %p As a supplier + .bottom ALWAYS FREE + %p.description + Add your products to Open Food Network, allowing customers to see your product range, and allowing you to act as a supplier to other shopfronts. + + .full_hub.option.one-third.column.omega.disabled + %a.full-width.button.selector{ ng: { click: "sells='own';producer_profile_only=false;", class: "{selected: sells=='own' && producer_profile_only==false}" } } + .top + %h3 Sell products + %p Through an OFN shopfront + .bottom 30 DAY TRIAL + %p.description + Test out having your own shopfront with full access to all Shopfront features for 30 days. After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. + + - else + .shop_profile.option.one-third.column.alpha + %a.full-width.button.selector{ ng: { class: "{selected: sells=='none'}" } } + .top + %h3 Shop Profile + %p Get a listing + .bottom ALWAYS FREE + %p.description + You want to use OFN as a place for people to find and contact you. + + .sixteen.columns.alpha + .admin-cta + %span.error{ ng: { show: "(enterprise.sells.$error.pattern || enterprise.sells.$error.pattern) && submitted" } } + Please choose one of the options above. + %input{ type: 'submit', value: 'Next', ng: { click: "submit(enterprise)" } } \ No newline at end of file diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index b5b031eefb..1934db1706 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -10,55 +10,4 @@ %h2 Next step %p Choose your starting point: - = form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), html: { name: "enterprise", novalidate: true } do |enterprise_form| - -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation - %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } - .options.sixteen.columns.alpha - - if @enterprise.is_primary_producer - %input{ type: 'checkbox', hidden: true, name: "producer_profile_only", ng: { required: true, model: 'producer_profile_only', value: "producer_profile_only"} } - .basic_producer.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=true;", class: "{selected: sells=='none' && producer_profile_only==true}" } } - .top - %h3 Producer Profile - %p Connect through OFN - .bottom ALWAYS FREE - %p.description - You want to use Open Food Network as a place for people to find and contact you. - - .producer_shop.option.one-third.column - %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=false;", class: "{selected: sells=='none' && producer_profile_only==false}" } } - .top - %h3 Sell products - %p As a supplier - .bottom ALWAYS FREE - %p.description - Add your products to Open Food Network, allowing customers to see your product range, and allowing you to act as a supplier to other shopfronts. - - .full_hub.option.one-third.column.omega.disabled - %a.full-width.button.selector{ ng: { click: "sells='own';producer_profile_only=false;", class: "{selected: sells=='own' && producer_profile_only==false}" } } - .top - %h3 Sell products - %p Through an OFN shopfront - .bottom 30 DAY TRIAL - %p.description - Test out having your own shopfront with full access to all Shopfront features for 30 days. After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. - - - else - .shop_profile.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { class: "{selected: sells=='none'}" } } - .top - %h3 Shop Profile - %p Get a listing - .bottom ALWAYS FREE - %p.description - You want to use OFN as a place for people to find and contact you. - - .sixteen.columns.alpha - .admin-cta - %span.error{ ng: { show: "(enterprise.sells.$error.pattern || enterprise.sells.$error.pattern) && submitted" } } - Please choose one of the options above. - %input{ type: 'submit', value: 'Next', ng: { click: "submit(enterprise)" } } - - - - + = render partial: "change_type_form" From 46ab42465534b757d0eceae6d096bec93fabcb0e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 09:10:45 +1100 Subject: [PATCH 095/681] Moving welcomeCrtl to enterprise module as sellsCtrl --- app/assets/javascripts/admin/all.js | 1 - .../controllers/sells_controller.js.coffee} | 4 ++-- app/assets/javascripts/admin/welcome/welcome.js.coffee | 1 - app/views/spree/admin/overview/_change_type_form.html.haml | 3 ++- app/views/spree/admin/overview/welcome.html.haml | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) rename app/assets/javascripts/admin/{welcome/controllers/welcome_controller.js.coffee => enterprises/controllers/sells_controller.js.coffee} (78%) delete mode 100644 app/assets/javascripts/admin/welcome/welcome.js.coffee diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index c674b1daae..f8d3ceb721 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -23,7 +23,6 @@ //= require ./products/products //= require ./shipping_methods/shipping_methods //= require ./users/users -//= require ./welcome/welcome //= require textAngular.min.js //= require textAngular-sanitize.min.js diff --git a/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/sells_controller.js.coffee similarity index 78% rename from app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee rename to app/assets/javascripts/admin/enterprises/controllers/sells_controller.js.coffee index 8db38181c4..37e16fc012 100644 --- a/app/assets/javascripts/admin/welcome/controllers/welcome_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/sells_controller.js.coffee @@ -1,5 +1,5 @@ -angular.module("admin.welcome") - .controller "welcomeCtrl", ($scope) -> +angular.module("admin.enterprises") + .controller "sellsCtrl", ($scope) -> $scope.sells = "unspecified" $scope.producer_profile_only = true $scope.submitted = false diff --git a/app/assets/javascripts/admin/welcome/welcome.js.coffee b/app/assets/javascripts/admin/welcome/welcome.js.coffee deleted file mode 100644 index ccb0fb5d0a..0000000000 --- a/app/assets/javascripts/admin/welcome/welcome.js.coffee +++ /dev/null @@ -1 +0,0 @@ -angular.module("admin.welcome", []) \ No newline at end of file diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml index 041c0b15e2..7f3108f90a 100644 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -1,4 +1,5 @@ -= form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), html: { name: "enterprise", novalidate: true } do |enterprise_form| += form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), + html: { name: "enterprise", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'sellsCtrl' } do |enterprise_form| -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } .options.sixteen.columns.alpha diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/spree/admin/overview/welcome.html.haml index 1934db1706..cfe5593d99 100644 --- a/app/views/spree/admin/overview/welcome.html.haml +++ b/app/views/spree/admin/overview/welcome.html.haml @@ -1,4 +1,4 @@ -#welcome_page.sixteen.columns.alpha{ ng: { app: "admin.welcome", controller: 'welcomeCtrl' } } +#welcome_page.sixteen.columns.alpha %header %h1 Welcome to the Open Food Network! %p From f6b0324456547e38c2043c885e507d0a0d4d633e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 10:43:04 +1100 Subject: [PATCH 096/681] Add change type form to single enterprise dashboard --- .../admin/change_type_form.css.sass | 71 +++++++++ app/assets/stylesheets/admin/welcome.css.sass | 135 ------------------ .../overview/_change_type_form.html.haml | 6 +- .../single_enterprise_dashboard.html.haml | 16 ++- 4 files changed, 86 insertions(+), 142 deletions(-) create mode 100644 app/assets/stylesheets/admin/change_type_form.css.sass diff --git a/app/assets/stylesheets/admin/change_type_form.css.sass b/app/assets/stylesheets/admin/change_type_form.css.sass new file mode 100644 index 0000000000..0bd49b25f6 --- /dev/null +++ b/app/assets/stylesheets/admin/change_type_form.css.sass @@ -0,0 +1,71 @@ +@import ../darkswarm/branding +@import ../darkswarm/mixins + +#change_type + section + margin: 2em 0 0 0 + &, & * + color: #5498da + + .description + background-color: #eff5fc + margin-top: -2em + margin-bottom: 2em + padding: 4em 2em 2em 1em + + .admin-cta + border: 1px solid #5498da + @include border-radius(3px) + text-align: center + padding: 1em + + .error + display: block + color: #f57e80 + border: 1px solid #f57e80 + background-color: #fde6e7 + @include border-radius(3px) + margin-bottom: 1em + padding: 0.5em + + a.selector + position: relative + border: 2px solid black + text-align: center + width: 100% + cursor: pointer + &, & * + color: white + &:after, &:before + top: 100% + left: 50% + border: solid transparent + content: " " + height: 0 + width: 0 + position: absolute + pointer-events: none + &:after + border-color: rgba(136, 183, 213, 0) + border-top-color: #5498da + border-width: 12px + margin-left: -12px + &:hover + &:after + border-top-color: #9fc820 + &:before + border-color: rgba(84, 152, 218, 0) + border-top-color: black + border-width: 15px + margin-left: -15px + .bottom + background: repeating-linear-gradient(60deg, rgba(84, 152, 218, 0), rgba(84, 152, 218, 0) 5px, rgba(255, 255, 255, 0.25) 5px, rgba(255, 255, 255, 0.25) 10px) + margin-top: 1em + margin-left: -15px + margin-right: -15px + padding: 5px + text-transform: uppercase + &.selected + background-color: black + &:after, &:hover &:after + border-top-color: black \ No newline at end of file diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass index 2b208a578e..c7d084766f 100644 --- a/app/assets/stylesheets/admin/welcome.css.sass +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -15,138 +15,3 @@ font-weight: 300 &, & * color: white - section - margin: 2em 0 0 0 - &, & * - color: #5498da - - .description - background-color: #eff5fc - margin-top: -2em - margin-bottom: 2em - padding: 4em 2em 2em 1em - - .admin-cta - border: 1px solid #5498da - @include border-radius(3px) - text-align: center - padding: 1em - - .error - display: block - color: #f57e80 - border: 1px solid #f57e80 - background-color: #fde6e7 - @include border-radius(3px) - margin-bottom: 1em - padding: 0.5em - - a.selector - position: relative - border: 2px solid black - text-align: center - width: 100% - cursor: pointer - &, & * - color: white - &:after, &:before - top: 100% - left: 50% - border: solid transparent - content: " " - height: 0 - width: 0 - position: absolute - pointer-events: none - &:after - border-color: rgba(136, 183, 213, 0) - border-top-color: #5498da - border-width: 12px - margin-left: -12px - &:hover - &:after - border-top-color: #9fc820 - &:before - border-color: rgba(84, 152, 218, 0) - border-top-color: black - border-width: 15px - margin-left: -15px - .bottom - background: repeating-linear-gradient(60deg, rgba(84, 152, 218, 0), rgba(84, 152, 218, 0) 5px, rgba(255, 255, 255, 0.25) 5px, rgba(255, 255, 255, 0.25) 10px) - margin-top: 1em - margin-left: -15px - margin-right: -15px - padding: 5px - text-transform: uppercase - &.selected - background-color: black - &:after, &:hover &:after - border-top-color: black - - - -// #welcome_page -// color: #000000 -// h1, h2, h3, h4, h5, h6 -// color: #000000 - -// .big -// font-weight: bold -// font-size: 36px - -// .plain_text -// font-size: 16px - -// #welcome -// text-align: center -// margin: 50px 0px -// h1 -// margin-bottom: 10px - -// #next_steps -// text-align: center -// margin-bottom: 50px -// h1 -// margin-bottom: 10px - -// .options -// .option -// a.selector -// opacity: 1 -// display: block -// cursor: pointer -// border: 3px solid black -// border-radius: 12px 12px 0px 0px -// text-align: center -// margin-bottom: 20px -// .top -// min-height: 113px -// padding: 15px 8px -// h2 -// margin-bottom: 10px -// .bottom -// border-top: 3px solid black -// padding: 8px 0px - -// &:hover -// opacity: 0.8 - -// &.selected -// color: #ffffff -// background-color: #ff4444 -// border-color: #000000 -// h1, h2, h3, h4, h5, h6 -// color: #ffffff - -// &.disabled -// color: #b0b0b0 -// .selector -// border-color: #b0b0b0 -// h1, h2, h3, h4, h5, h6 -// color: #b0b0b0 -// .bottom -// border-top-color: #b0b0b0 - -// .description -// font-size: 16px -// text-align: justify \ No newline at end of file diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml index 7f3108f90a..0b1bbfabb8 100644 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -1,5 +1,5 @@ = form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), - html: { name: "enterprise", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'sellsCtrl' } do |enterprise_form| + html: { name: "change_type", id: "change_type", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'sellsCtrl' } do |change_type_form| -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } .options.sixteen.columns.alpha @@ -44,6 +44,6 @@ .sixteen.columns.alpha .admin-cta - %span.error{ ng: { show: "(enterprise.sells.$error.pattern || enterprise.sells.$error.pattern) && submitted" } } + %span.error{ ng: { show: "(change_type.sells.$error.pattern || change_type.sells.$error.pattern) && submitted" } } Please choose one of the options above. - %input{ type: 'submit', value: 'Next', ng: { click: "submit(enterprise)" } } \ No newline at end of file + %input{ type: 'submit', value: 'Next', ng: { click: "submit(change_type)" } } \ No newline at end of file diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index b24d574c24..c2d549ea4e 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -1,5 +1,5 @@ - content_for :page_title do - %h1 + %h1 = @enterprise.name %span.small ( @@ -7,8 +7,16 @@ ) - content_for :page_actions do - -# We need an action to render the welcome template, that this button can link to - %button Change type + :javascript + function toggleType(){ + $("#type_selection").slideToggle() + } + #type_button + %button#toggle_type{ onClick: 'toggleType()' } Change type + + +#type_selection{ hidden: true } + = render partial: "change_type_form" #message Message bar @@ -41,4 +49,4 @@ - if can? :admin, OrderCycle Add and manage order cycles %br/ - = link_to "Manage order cycles", main_app.admin_order_cycles_path + = link_to "Manage order cycles", main_app.admin_order_cycles_path \ No newline at end of file From 49a2b774b0d5cdea551fc455580f057eb859a119 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 10:56:03 +1100 Subject: [PATCH 097/681] Tweak language in helper --- app/helpers/enterprises_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index 91821a3914..3ad7a5051a 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -23,7 +23,7 @@ module EnterprisesHelper # TODO: When we can distinguish between profiles and producers that supply only (without # their own store), include it here. # profile, supplier only, shopfront - enterprise.sells == 'none' ? 'profile' : 'shopfront' + enterprise.sells == 'none' ? 'Profile' : 'Has Shopfront' end From f03c7ba02db8c6e02ea9f21f655d1e4cb1d0d91d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 11:02:16 +1100 Subject: [PATCH 098/681] Rename sellCtrl to changeTypeFormCtrl --- ...roller.js.coffee => change_type_form_controller.js.coffee} | 2 +- app/views/spree/admin/overview/_change_type_form.html.haml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) rename app/assets/javascripts/admin/enterprises/controllers/{sells_controller.js.coffee => change_type_form_controller.js.coffee} (86%) diff --git a/app/assets/javascripts/admin/enterprises/controllers/sells_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/change_type_form_controller.js.coffee similarity index 86% rename from app/assets/javascripts/admin/enterprises/controllers/sells_controller.js.coffee rename to app/assets/javascripts/admin/enterprises/controllers/change_type_form_controller.js.coffee index 37e16fc012..02bd5e253c 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/sells_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/change_type_form_controller.js.coffee @@ -1,5 +1,5 @@ angular.module("admin.enterprises") - .controller "sellsCtrl", ($scope) -> + .controller "changeTypeFormCtrl", ($scope) -> $scope.sells = "unspecified" $scope.producer_profile_only = true $scope.submitted = false diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml index 0b1bbfabb8..4952c9dff4 100644 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -1,5 +1,7 @@ += admin_inject_enterprise + = form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), - html: { name: "change_type", id: "change_type", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'sellsCtrl' } do |change_type_form| + html: { name: "change_type", id: "change_type", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'changeTypeFormCtrl' } do |change_type_form| -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } .options.sixteen.columns.alpha From 911d1e3dc4ada36585fd335f18ca0e3ffda6af37 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 11:02:48 +1100 Subject: [PATCH 099/681] Add down chevron to change type button on single ent dash --- .../admin/overview/single_enterprise_dashboard.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index c2d549ea4e..6df9c751ac 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -12,7 +12,9 @@ $("#type_selection").slideToggle() } #type_button - %button#toggle_type{ onClick: 'toggleType()' } Change type + %button#toggle_type{ onClick: 'toggleType()' } + Change type + %i.icon-chevron-down #type_selection{ hidden: true } From 900ef4ddccb8a8bad8b31fb6332874803f18c2fa Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 11:12:54 +1100 Subject: [PATCH 100/681] Change type form is aware of existing state of enterprise --- .../controllers/change_type_form_controller.js.coffee | 6 +++--- app/serializers/api/admin/enterprise_serializer.rb | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/change_type_form_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/change_type_form_controller.js.coffee index 02bd5e253c..c2f96f254f 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/change_type_form_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/change_type_form_controller.js.coffee @@ -1,7 +1,7 @@ angular.module("admin.enterprises") - .controller "changeTypeFormCtrl", ($scope) -> - $scope.sells = "unspecified" - $scope.producer_profile_only = true + .controller "changeTypeFormCtrl", ($scope, enterprise) -> + $scope.sells = enterprise.sells + $scope.producer_profile_only = enterprise.producer_profile_only $scope.submitted = false $scope.valid = (form) -> diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index e6d00b7aae..a23764b84a 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -1,3 +1,4 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids + attributes :producer_profile_only end From f40b4d9d03690f4775a9fa331e8cc50eb6973964 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 11:22:57 +1100 Subject: [PATCH 101/681] Different text for CTA button depending on context --- app/views/spree/admin/overview/_change_type_form.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml index 4952c9dff4..08f220f9e1 100644 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -48,4 +48,7 @@ .admin-cta %span.error{ ng: { show: "(change_type.sells.$error.pattern || change_type.sells.$error.pattern) && submitted" } } Please choose one of the options above. - %input{ type: 'submit', value: 'Next', ng: { click: "submit(change_type)" } } \ No newline at end of file + - if @enterprise.sells == 'unspecified' + %input{ type: 'submit', value: 'Next', ng: { click: "submit(change_type)" } } + - else + %input{ type: 'submit', value: 'Change Type', ng: { click: "submit(change_type)" } } \ No newline at end of file From 0b3e293b3807e96d97de8c2eb921d5dd191c0fb4 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 11:26:58 +1100 Subject: [PATCH 102/681] Remove whitespace, add alert box --- .../overview/single_enterprise_dashboard.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index c2d549ea4e..5c517242a0 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -1,10 +1,8 @@ - content_for :page_title do %h1 = @enterprise.name - %span.small - ( - = enterprise_type_name(@enterprise) - ) + %span.small< + = "(#{enterprise_type_name(@enterprise)})" - content_for :page_actions do :javascript @@ -18,8 +16,10 @@ #type_selection{ hidden: true } = render partial: "change_type_form" -#message - Message bar +.alert-box{"data-alert" => ""} + Alert box. Timely and contextual messages go here. Should animate in and be closeable. + %a.close{:href => "#"} × + .row .alpha.seven.columns From 76c6c260d1b65db43e3bc64feba42552e4e1426f Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 11:48:13 +1100 Subject: [PATCH 103/681] Add row containers to make grid nesting work --- .../overview/_change_type_form.html.haml | 84 ++++++++++--------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml index 0b1bbfabb8..82a89e46de 100644 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -2,48 +2,50 @@ html: { name: "change_type", id: "change_type", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'sellsCtrl' } do |change_type_form| -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } - .options.sixteen.columns.alpha - - if @enterprise.is_primary_producer - %input{ type: 'checkbox', hidden: true, name: "producer_profile_only", ng: { required: true, model: 'producer_profile_only', value: "producer_profile_only"} } - .basic_producer.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=true;", class: "{selected: sells=='none' && producer_profile_only==true}" } } - .top - %h3 Producer Profile - %p Connect through OFN - .bottom ALWAYS FREE - %p.description - You want to use Open Food Network as a place for people to find and contact you. - .producer_shop.option.one-third.column - %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=false;", class: "{selected: sells=='none' && producer_profile_only==false}" } } - .top - %h3 Sell products - %p As a supplier - .bottom ALWAYS FREE - %p.description - Add your products to Open Food Network, allowing customers to see your product range, and allowing you to act as a supplier to other shopfronts. + .row + .options.sixteen.columns.alpha + - if @enterprise.is_primary_producer + %input{ type: 'checkbox', hidden: true, name: "producer_profile_only", ng: { required: true, model: 'producer_profile_only', value: "producer_profile_only"} } + .basic_producer.option.one-third.column.alpha + %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=true;", class: "{selected: sells=='none' && producer_profile_only==true}" } } + .top + %h3 Producer Profile + %p Connect through OFN + .bottom ALWAYS FREE + %p.description + You want to use Open Food Network as a place for people to find and contact you. - .full_hub.option.one-third.column.omega.disabled - %a.full-width.button.selector{ ng: { click: "sells='own';producer_profile_only=false;", class: "{selected: sells=='own' && producer_profile_only==false}" } } - .top - %h3 Sell products - %p Through an OFN shopfront - .bottom 30 DAY TRIAL - %p.description - Test out having your own shopfront with full access to all Shopfront features for 30 days. After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. + .producer_shop.option.one-third.column + %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=false;", class: "{selected: sells=='none' && producer_profile_only==false}" } } + .top + %h3 Sell products + %p As a supplier + .bottom ALWAYS FREE + %p.description + Add your products to Open Food Network, allowing customers to see your product range, and allowing you to act as a supplier to other shopfronts. - - else - .shop_profile.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { class: "{selected: sells=='none'}" } } - .top - %h3 Shop Profile - %p Get a listing - .bottom ALWAYS FREE - %p.description - You want to use OFN as a place for people to find and contact you. + .full_hub.option.one-third.column.omega.disabled + %a.full-width.button.selector{ ng: { click: "sells='own';producer_profile_only=false;", class: "{selected: sells=='own' && producer_profile_only==false}" } } + .top + %h3 Sell products + %p Through an OFN shopfront + .bottom 30 DAY TRIAL + %p.description + Test out having your own shopfront with full access to all Shopfront features for 30 days. After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. - .sixteen.columns.alpha - .admin-cta - %span.error{ ng: { show: "(change_type.sells.$error.pattern || change_type.sells.$error.pattern) && submitted" } } - Please choose one of the options above. - %input{ type: 'submit', value: 'Next', ng: { click: "submit(change_type)" } } \ No newline at end of file + - else + .shop_profile.option.one-third.column.alpha + %a.full-width.button.selector{ ng: { class: "{selected: sells=='none'}" } } + .top + %h3 Shop Profile + %p Get a listing + .bottom ALWAYS FREE + %p.description + You want to use OFN as a place for people to find and contact you. + .row + .sixteen.columns.alpha + .admin-cta + %span.error{ ng: { show: "(change_type.sells.$error.pattern || change_type.sells.$error.pattern) && submitted" } } + Please choose one of the options above. + %input{ type: 'submit', value: 'Next', ng: { click: "submit(change_type)" } } \ No newline at end of file From 982be3a5635baa93c8f42530545d917ba9013530 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 11:48:39 +1100 Subject: [PATCH 104/681] Stylesheet for single enterprise user dashboard components --- .../admin/dashboard-single-ent.css.sass | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/assets/stylesheets/admin/dashboard-single-ent.css.sass diff --git a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass new file mode 100644 index 0000000000..0052c6c58b --- /dev/null +++ b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass @@ -0,0 +1,16 @@ +@import ../darkswarm/branding +@import ../darkswarm/mixins + + +.alert-box + display: block + background-color: #eff5dc + border: 1px solid #9fc820 + color: #666 + margin-top: 1em + margin-bottom: 1em + @include border-radius(3px) + transition: opacity 300ms ease-out + padding: 0.77778em 1.33333em 0.77778em 0.77778em + a.close + float: right \ No newline at end of file From 631386ced493a7eb7abc75c055c2f3ffc913c4dd Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 11:48:53 +1100 Subject: [PATCH 105/681] Tweaking markup --- .../spree/admin/overview/single_enterprise_dashboard.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 5c517242a0..4e5c1eb45c 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -16,6 +16,7 @@ #type_selection{ hidden: true } = render partial: "change_type_form" +// Contextual messages. Hide if not being used: .alert-box{"data-alert" => ""} Alert box. Timely and contextual messages go here. Should animate in and be closeable. %a.close{:href => "#"} × @@ -29,6 +30,7 @@ = link_to "#{@enterprise.name} live", main_app.map_path .two.columns +   .seven.columns.omega Change or tweak any of the details of your profile From d0c3502f27c8685c22677d1546607a7d5c3bf2a1 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 12:32:05 +1100 Subject: [PATCH 106/681] Adding trial progress bar --- .../admin/components/trial_progess_bar.sass | 8 ++++++++ app/helpers/enterprises_helper.rb | 12 ++++++------ app/models/enterprise.rb | 11 +++++++++++ .../admin/add_trial_progress_bar.html.haml.deface | 6 ++++++ .../spree/admin/shared/_trial_progress_bar.html.haml | 2 ++ 5 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 app/assets/stylesheets/admin/components/trial_progess_bar.sass create mode 100644 app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface create mode 100644 app/views/spree/admin/shared/_trial_progress_bar.html.haml diff --git a/app/assets/stylesheets/admin/components/trial_progess_bar.sass b/app/assets/stylesheets/admin/components/trial_progess_bar.sass new file mode 100644 index 0000000000..37ef4642d9 --- /dev/null +++ b/app/assets/stylesheets/admin/components/trial_progess_bar.sass @@ -0,0 +1,8 @@ +#trial_progress_bar + position: fixed + bottom: 0px + width: 100% + padding: 8px 10px + font-weight: bold + background-color: #5498da + color: white diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index 91821a3914..1421594e97 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -10,7 +10,7 @@ module EnterprisesHelper def managed_enterprises Enterprise.managed_by(spree_current_user) end - + def enterprises_options enterprises enterprises.map { |enterprise| [enterprise.name + ": " + enterprise.address.address1 + ", " + enterprise.address.city, enterprise.id.to_i] } end @@ -20,13 +20,13 @@ module EnterprisesHelper end def enterprise_type_name(enterprise) - # TODO: When we can distinguish between profiles and producers that supply only (without - # their own store), include it here. - # profile, supplier only, shopfront - enterprise.sells == 'none' ? 'profile' : 'shopfront' + if enterprise.sells == 'none' + enterprise.producer_profile_only ? 'Profile' : 'Supplier Only' + else + "Has Shopfront" + end end - def enterprise_confirm_delete_message(enterprise) if enterprise.supplied_products.present? "This will also delete the #{pluralize enterprise.supplied_products.count, 'product'} that this enterprise supplies. Are you sure you want to continue?" diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index aa3a6043ef..9ff835b868 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -1,5 +1,6 @@ class Enterprise < ActiveRecord::Base SELLS = %w(unspecified none own any) + SHOP_TRIAL_LENGTH = 30 ENTERPRISE_SEARCH_RADIUS = 100 devise :confirmable, reconfirmable: true @@ -259,6 +260,16 @@ class Enterprise < ActiveRecord::Base select('DISTINCT spree_taxons.*') end + def shop_trial_in_progress? + !!shop_trial_start_date && + (shop_trial_start_date + SHOP_TRIAL_LENGTH.days > Time.now) && + %w(own any).include?(sells) + end + + def remaining_trial_days + distance_of_time_in_words(Time.now, shop_trial_start_date + SHOP_TRIAL_LENGTH.days) + end + protected def devise_mailer diff --git a/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface b/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface new file mode 100644 index 0000000000..bcd787cf28 --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface @@ -0,0 +1,6 @@ +/ insert_top "[data-hook='admin_footer_scripts']" + +- enterprise = spree_current_user.enterprises.first if OpenFoodNetwork::Permissions.new(spree_current_user).manages_one_enterprise? + +- if enterprise && enterprise.shop_trial_in_progress? + = render 'spree/admin/shared/trial_progress_bar', enterprise: enterprise \ No newline at end of file diff --git a/app/views/spree/admin/shared/_trial_progress_bar.html.haml b/app/views/spree/admin/shared/_trial_progress_bar.html.haml new file mode 100644 index 0000000000..183c63a901 --- /dev/null +++ b/app/views/spree/admin/shared/_trial_progress_bar.html.haml @@ -0,0 +1,2 @@ +#trial_progress_bar + = "Your shopfront trial expires in #{enterprise.remaining_trial_days}." \ No newline at end of file From e32ab7b4b7f8bac0f391a55e8947b4ede7b3d58b Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 12:39:08 +1100 Subject: [PATCH 107/681] Single enterprise dashboard styling --- .../stylesheets/admin/dashboard-single-ent.css.sass | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass index 0052c6c58b..a75c424085 100644 --- a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass +++ b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass @@ -13,4 +13,13 @@ transition: opacity 300ms ease-out padding: 0.77778em 1.33333em 0.77778em 0.77778em a.close - float: right \ No newline at end of file + float: right + + +.dashboard_item + .header + padding: 0.77778em 1.33333em 0.77778em 0.77778em + height: auto !important + .list + .button.bottom + width: 100% \ No newline at end of file From 68cb3efc506f3b30871c9826516db8e691bcf8e5 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 12:39:25 +1100 Subject: [PATCH 108/681] Markup fixes for single enterprise dashboard (first state) --- .../single_enterprise_dashboard.html.haml | 74 +++++++++++++------ 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 4e5c1eb45c..56c243a6c6 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -23,32 +23,60 @@ .row - .alpha.seven.columns - See your profile live on the Open Food Network map. - %br/ - -# Can we pass an anchor here to zoom to our enterprise? - = link_to "#{@enterprise.name} live", main_app.map_path + .alpha.seven.columns.dashboard_item + .header + %h3 + %span.icon-map-marker + Your profile live + %p on the Open Food Network map + .list + /-# Can we pass an anchor here to zoom to our enterprise? + %a.button.bottom{href: main_app.map_path, target: '_blank'} + See + = @enterprise.name + live + %span.icon-arrow-right + .two.columns +   + + .seven.columns.omega.dashboard_item + .header + %h3 + %span.icon-edit + Edit profile details + %p Change your profile description, images, etc. + .list + %a.button.bottom{href: main_app.edit_admin_enterprise_path(@enterprise)} + Manage + = @enterprise.name + %span.icon-arrow-right + +.row + .seven.columns.alpha.dashboard_item + - if can? :admin, Spree::Product + .header + %h3 + %span.icon-plus + Add & manage products + .list + %a.button.bottom{href: bulk_edit_admin_products_path} + Manage products + %span.icon-arrow-right .two.columns   - .seven.columns.omega - Change or tweak any of the details of your profile - %br/ - = link_to "Manage #{@enterprise.name}", main_app.edit_admin_enterprise_path(@enterprise) - - -.row - .alpha.seven.columns - - if can? :admin, Spree::Product - Add and manage products - %br/ - = link_to "Manage products", bulk_edit_admin_products_path - - .two.columns - .seven.columns.omega - if can? :admin, OrderCycle - Add and manage order cycles - %br/ - = link_to "Manage order cycles", main_app.admin_order_cycles_path \ No newline at end of file + .header + %h3 + %span.icon-shopping-cart + Add & manage order cycles + .list + %a.button.bottom{href: main_app.admin_order_cycles_path} + Manage order cycles + %span.icon-arrow-right + + + + From b3ac06e46c557822ba1a7e2baecefb9c0296f429 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 12:42:14 +1100 Subject: [PATCH 109/681] Fix icon type to match nav bar --- .../spree/admin/overview/single_enterprise_dashboard.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 56c243a6c6..fb869b81fc 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -56,7 +56,7 @@ - if can? :admin, Spree::Product .header %h3 - %span.icon-plus + %span.icon-th-large Add & manage products .list %a.button.bottom{href: bulk_edit_admin_products_path} From b172c4fd4f895cb2da9aa783706d40d9409071f1 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 12:53:04 +1100 Subject: [PATCH 110/681] Search query make empty string rather than undefined --- app/assets/javascripts/admin/bulk_product_update.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index b68e3340e1..c95f27932e 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -43,6 +43,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.currentFilters = [] $scope.limit = 15 $scope.productsWithUnsavedVariants = [] + $scope.query = "" $scope.initialise = -> authorise_api_reponse = "" From 01cb39a93fbcdac76616fff5bff4155e0dd3445b Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 12:54:06 +1100 Subject: [PATCH 111/681] Error messages specific to use case --- .../admin/products/bulk_edit/_indicators.html.haml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/views/spree/admin/products/bulk_edit/_indicators.html.haml b/app/views/spree/admin/products/bulk_edit/_indicators.html.haml index 97c8a45aa2..d6ce039231 100644 --- a/app/views/spree/admin/products/bulk_edit/_indicators.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_indicators.html.haml @@ -5,5 +5,12 @@ %img.spinner{ src: "/assets/loading.gif" } %h1 LOADING PRODUCTS -%div.sixteen.columns.alpha{ 'ng-show' => '!loading && filteredProducts.length == 0' } - %h1#no_results No products found. +%div.sixteen.columns.alpha{ 'ng-show' => '!loading && filteredProducts.length == 0 && query.length==0' } + %h1#no_results No products yet. Why don't you add some? + +%div.sixteen.columns.alpha{ 'ng-show' => '!loading && filteredProducts.length == 0 && query.length!=0' } + %h1#no_results + Sorry, no results match + ' + {{query}} + ' \ No newline at end of file From 498b51cedb93a4ddb125df83675e37477a2d9363 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 12:54:21 +1100 Subject: [PATCH 112/681] Switching out chevron on Change Type button --- .../admin/overview/single_enterprise_dashboard.html.haml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 6df9c751ac..9589333af2 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -9,6 +9,12 @@ - content_for :page_actions do :javascript function toggleType(){ + if( $('#type_selection').is(":visible") ){ + $('button#toggle_type i').switchClass("icon-chevron-up","icon-chevron-down") + } + else { + $('button#toggle_type i').switchClass("icon-chevron-down","icon-chevron-up") + } $("#type_selection").slideToggle() } #type_button From 0d73d8f0431a01f3c86cba8936274c8dea4cce3c Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 13:01:56 +1100 Subject: [PATCH 113/681] Fix layout add required class --- .../spree/admin/overview/single_enterprise_dashboard.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index fb869b81fc..d6adb4a8de 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -66,7 +66,7 @@ .two.columns   - .seven.columns.omega + .seven.columns.omega.dashboard_item - if can? :admin, OrderCycle .header %h3 From b0d8290dab5b24ffac9f5fa007dc43cca0aeb638 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 13:28:25 +1100 Subject: [PATCH 114/681] Add alert for visibility to single ent dash --- .../single_enterprise_dashboard.html.haml | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index e8565a599b..829754d4a0 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -24,16 +24,23 @@ #type_selection{ hidden: true } = render partial: "change_type_form" -// Contextual messages. Hide if not being used: -.alert-box{"data-alert" => ""} - Alert box. Timely and contextual messages go here. Should animate in and be closeable. - %a.close{:href => "#"} × - + +- if @enterprise.visible + .alert-box + %strong Hint: + To allow people to find you, turn on your visibility under + %strong Edit profile details. + %a.close{ href: "#" } × + + :javascript + $('a.close').click(function(){ + $(this).parent().slideUp(250); + }); .row .alpha.seven.columns.dashboard_item - .header - %h3 + .header + %h3 %span.icon-map-marker Your profile live %p on the Open Food Network map @@ -41,29 +48,29 @@ /-# Can we pass an anchor here to zoom to our enterprise? %a.button.bottom{href: main_app.map_path, target: '_blank'} See - = @enterprise.name + = @enterprise.name live %span.icon-arrow-right .two.columns   .seven.columns.omega.dashboard_item - .header - %h3 - %span.icon-edit + .header + %h3 + %span.icon-edit Edit profile details %p Change your profile description, images, etc. .list %a.button.bottom{href: main_app.edit_admin_enterprise_path(@enterprise)} Manage - = @enterprise.name + = @enterprise.name %span.icon-arrow-right .row .seven.columns.alpha.dashboard_item - if can? :admin, Spree::Product - .header - %h3 + .header + %h3 %span.icon-th-large Add & manage products .list @@ -76,8 +83,8 @@ .seven.columns.omega.dashboard_item - if can? :admin, OrderCycle - .header - %h3 + .header + %h3 %span.icon-shopping-cart Add & manage order cycles .list From 20e2cb4ecc996f590bf86daa57a482929b135ba3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 13:31:20 +1100 Subject: [PATCH 115/681] Set visibility to false for enterprises created through registration process --- app/controllers/api/enterprises_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/api/enterprises_controller.rb b/app/controllers/api/enterprises_controller.rb index b1570fb0d1..fb96bcd60d 100644 --- a/app/controllers/api/enterprises_controller.rb +++ b/app/controllers/api/enterprises_controller.rb @@ -4,6 +4,7 @@ module Api before_filter :override_owner, only: [:create, :update] before_filter :check_type, only: :update before_filter :override_sells, only: [:create, :update] + before_filter :override_visiblity, only: [:create, :update] respond_to :json def managed @@ -64,5 +65,9 @@ module Api def override_sells params[:enterprise][:sells] = 'unspecified' end + + def override_visible + params[:enterprise][:visible] = false + end end end From 3e002c6f82b4c9389c1c7ed39a2e7b6c3027a94e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 13:34:49 +1100 Subject: [PATCH 116/681] Fiddle with text --- .../admin/overview/single_enterprise_dashboard.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 829754d4a0..b6de336025 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -25,11 +25,11 @@ = render partial: "change_type_form" -- if @enterprise.visible +- if !@enterprise.visible .alert-box %strong Hint: To allow people to find you, turn on your visibility under - %strong Edit profile details. + %strong= "Manage #{@enterprise.name}." %a.close{ href: "#" } × :javascript From 96516a8ff365e4ac6909adf89fec817dd91dbce4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 14:12:47 +1100 Subject: [PATCH 117/681] User can switch between trialling a shopfront and not trialling a shopfront --- .../admin/enterprises_controller.rb | 11 ++++- .../admin/enterprises_controller_spec.rb | 42 +++++++++++++++---- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 8db4101075..b3fad9dc5c 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -19,12 +19,19 @@ module Admin def set_sells enterprise = Enterprise.find(params[:id]) attributes = { sells: params[:sells] } - attributes[:producer_profile_only] = !!params[:producer_profile_only] if params[:sells] == 'none' + attributes[:producer_profile_only] = params[:sells] == "none" && !!params[:producer_profile_only] attributes[:shop_trial_start_date] = Time.now if params[:sells] == "own" if %w(none own).include?(params[:sells]) if params[:sells] == 'own' && enterprise.shop_trial_start_date - flash[:error] = "You've already started your trial!" + expiry = enterprise.shop_trial_start_date + Enterprise::SHOP_TRIAL_LENGTH.days + if Time.now > expiry + flash[:error] = "Sorry, but you've already had a trial. Expired on: #{expiry.strftime('%Y-%m-%d')}" + else + attributes.delete :shop_trial_start_date + enterprise.update_attributes(attributes) + flash[:notice] = "Welcome back! Your trial expires on: #{expiry.strftime('%Y-%m-%d')}" + end elsif enterprise.update_attributes(attributes) flash[:success] = "Congratulations! Registration for #{enterprise.name} is complete!" end diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 60f0d9f736..1e961e3710 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -147,18 +147,44 @@ module Admin end context "setting 'sells' to 'own'" do - it "is disallowed if a trial already been started" do + before do enterprise.sells = 'own' - enterprise.shop_trial_start_date = Date.today.to_time enterprise.save! - spree_post :set_sells, { id: enterprise.id, sells: 'own' } - expect(response).to redirect_to spree.admin_path - expect(flash[:error]).to eq "You've already started your trial!" - expect(enterprise.reload.sells).to eq 'own' - expect(enterprise.reload.shop_trial_start_date).to eq Date.today.to_time end - context "if a trial has not already been started" do + context "if the trial has finished" do + before do + enterprise.shop_trial_start_date = (Date.today - 30.days).to_time + enterprise.save! + end + + it "is disallowed" do + spree_post :set_sells, { id: enterprise.id, sells: 'own' } + expect(response).to redirect_to spree.admin_path + trial_expiry = Date.today.strftime("%Y-%m-%d") + expect(flash[:error]).to eq "Sorry, but you've already had a trial. Expired on: #{trial_expiry}" + expect(enterprise.reload.sells).to eq 'own' + expect(enterprise.reload.shop_trial_start_date).to eq (Date.today - 30.days).to_time + end + end + + context "if the trial has not finished" do + before do + enterprise.shop_trial_start_date = Date.today.to_time + enterprise.save! + end + + it "is allowed, but trial start date is not reset" do + spree_post :set_sells, { id: enterprise.id, sells: 'own' } + expect(response).to redirect_to spree.admin_path + trial_expiry = (Date.today + 30.days).strftime("%Y-%m-%d") + expect(flash[:notice]).to eq "Welcome back! Your trial expires on: #{trial_expiry}" + expect(enterprise.reload.sells).to eq 'own' + expect(enterprise.reload.shop_trial_start_date).to eq Date.today.to_time + end + end + + context "if a trial has not started" do it "is allowed" do spree_post :set_sells, { id: enterprise.id, sells: 'own' } expect(response).to redirect_to spree.admin_path From 8ac367675f827da45d859814c281f11c051ce4c1 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 14:24:55 +1100 Subject: [PATCH 118/681] Adding big button class to CTA --- app/views/spree/admin/overview/_change_type_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml index 23b7809265..5af621b152 100644 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -51,6 +51,6 @@ %span.error{ ng: { show: "(change_type.sells.$error.pattern || change_type.sells.$error.pattern) && submitted" } } Please choose one of the options above. - if @enterprise.sells == 'unspecified' - %input{ type: 'submit', value: 'Next', ng: { click: "submit(change_type)" } } + %input.big_button{ type: 'submit', value: 'Next', ng: { click: "submit(change_type)" } } - else - %input{ type: 'submit', value: 'Change Type', ng: { click: "submit(change_type)" } } + %input.big_button{ type: 'submit', value: 'Change Type', ng: { click: "submit(change_type)" } } From d388747a81197a6c3e0f86fa671d6af6e0308565 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 14:48:29 +1100 Subject: [PATCH 119/681] Make margine appear for small devices only --- app/assets/stylesheets/admin/change_type_form.css.sass | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/admin/change_type_form.css.sass b/app/assets/stylesheets/admin/change_type_form.css.sass index 0bd49b25f6..bf67f89696 100644 --- a/app/assets/stylesheets/admin/change_type_form.css.sass +++ b/app/assets/stylesheets/admin/change_type_form.css.sass @@ -10,8 +10,9 @@ .description background-color: #eff5fc margin-top: -2em - margin-bottom: 2em padding: 4em 2em 2em 1em + @media all and (max-width: 786px) + margin-bottom: 2em .admin-cta border: 1px solid #5498da From c733ca251ee1f0dfac31fa2dba71bab5e0274444 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 14:48:46 +1100 Subject: [PATCH 120/681] Styling for big button --- .../stylesheets/admin/dashboard-single-ent.css.sass | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass index a75c424085..67f9a3e603 100644 --- a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass +++ b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass @@ -22,4 +22,10 @@ height: auto !important .list .button.bottom - width: 100% \ No newline at end of file + width: 100% + +.button.big + width: 100% + font-size: 1rem + @include border-radius(25px) + padding: 15px \ No newline at end of file From 2eb152f63efd2f20cbedec7ef4c496b680e18c60 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 14:49:09 +1100 Subject: [PATCH 121/681] Change class name for big button; change labels for submit buttons --- .../admin/overview/_change_type_form.html.haml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml index 5af621b152..41d6331a48 100644 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -47,10 +47,11 @@ You want to use OFN as a place for people to find and contact you. .row .sixteen.columns.alpha - .admin-cta - %span.error{ ng: { show: "(change_type.sells.$error.pattern || change_type.sells.$error.pattern) && submitted" } } - Please choose one of the options above. - - if @enterprise.sells == 'unspecified' - %input.big_button{ type: 'submit', value: 'Next', ng: { click: "submit(change_type)" } } - - else - %input.big_button{ type: 'submit', value: 'Change Type', ng: { click: "submit(change_type)" } } + %span.error{ ng: { show: "(change_type.sells.$error.pattern || change_type.sells.$error.pattern) && submitted" } } + Please choose one of the options above. + - if @enterprise.sells == 'unspecified' + %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)" } } + - else + %input.button.big{ type: 'submit', value: 'Change now', ng: { click: "submit(change_type)" } } + %br   + %hr From 8b819539e7c7a683e9d933e2e3b8599cd9dcf93a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 14:52:59 +1100 Subject: [PATCH 122/681] Fixing specs --- app/controllers/api/enterprises_controller.rb | 2 +- .../features/admin/bulk_product_update_spec.rb | 2 +- spec/features/admin/overview_spec.rb | 4 +++- spec/serializers/enterprise_serializer_spec.rb | 18 ------------------ 4 files changed, 5 insertions(+), 21 deletions(-) diff --git a/app/controllers/api/enterprises_controller.rb b/app/controllers/api/enterprises_controller.rb index fb96bcd60d..cba50c4923 100644 --- a/app/controllers/api/enterprises_controller.rb +++ b/app/controllers/api/enterprises_controller.rb @@ -4,7 +4,7 @@ module Api before_filter :override_owner, only: [:create, :update] before_filter :check_type, only: :update before_filter :override_sells, only: [:create, :update] - before_filter :override_visiblity, only: [:create, :update] + before_filter :override_visible, only: [:create, :update] respond_to :json def managed diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 8bb54cfca8..03a90137d9 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -25,7 +25,7 @@ feature %q{ it "displays a message when number of products is zero" do visit '/admin/products/bulk_edit' - expect(page).to have_text "No products found." + expect(page).to have_text "No products yet. Why don't you add some?" end it "displays a select box for suppliers, with the appropriate supplier selected" do diff --git a/spec/features/admin/overview_spec.rb b/spec/features/admin/overview_spec.rb index 184d68c80b..8f7063d47e 100644 --- a/spec/features/admin/overview_spec.rb +++ b/spec/features/admin/overview_spec.rb @@ -28,11 +28,13 @@ feature %q{ end end - context "with an enterprise" do + context "with multiple enterprises" do let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } before :each do @enterprise_user.enterprise_roles.build(enterprise: d1).save + @enterprise_user.enterprise_roles.build(enterprise: d2).save end it "displays information about the enterprise" do diff --git a/spec/serializers/enterprise_serializer_spec.rb b/spec/serializers/enterprise_serializer_spec.rb index f3409f2d57..1063a042e7 100644 --- a/spec/serializers/enterprise_serializer_spec.rb +++ b/spec/serializers/enterprise_serializer_spec.rb @@ -18,22 +18,4 @@ describe Api::EnterpriseSerializer do serializer = Api::EnterpriseSerializer.new enterprise serializer.to_json.should match "map_005-hub.svg" end - - describe "visibility" do - before do - enterprise.stub(:visible).and_return true - end - - it "is visible when confirmed" do - enterprise.stub(:confirmed?).and_return true - serializer = Api::EnterpriseSerializer.new enterprise - expect(serializer.to_json).to match "\"visible\":true" - end - - it "is not visible when unconfirmed" do - enterprise.stub(:confirmed?).and_return false - serializer = Api::EnterpriseSerializer.new enterprise - expect(serializer.to_json).to match "\"visible\":false" - end - end end From a1575289743b8517668246813aab5a32181fc513 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 15:27:08 +1100 Subject: [PATCH 123/681] Adding specs for single ent dash and remove product ability for profiles --- app/models/spree/ability_decorator.rb | 5 +- .../overview/_change_type_form.html.haml | 4 +- .../single_enterprise_dashboard.html.haml | 19 ++++--- spec/features/admin/overview_spec.rb | 50 +++++++++++++++++++ spec/models/spree/ability_spec.rb | 26 ++++++++-- 5 files changed, 91 insertions(+), 13 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index e3c90c7aa3..35652e7348 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -21,9 +21,10 @@ class AbilityDecorator user.enterprises.present? end - # Users can manage products if they have an enterprise. + # Users can manage products if they have an enterprise that is not a profile. def can_manage_products?(user) - can_manage_enterprises? user + can_manage_enterprises?(user) && + user.enterprises.any? { |e| e.category != :hub_profile && e.producer_profile_only != true } end # Users can manage orders if they have a sells own/any enterprise. diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml index 5af621b152..4be3ecda65 100644 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -38,7 +38,7 @@ - else .shop_profile.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { class: "{selected: sells=='none'}" } } + %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } .top %h3 Shop Profile %p Get a listing @@ -48,7 +48,7 @@ .row .sixteen.columns.alpha .admin-cta - %span.error{ ng: { show: "(change_type.sells.$error.pattern || change_type.sells.$error.pattern) && submitted" } } + %span.error{ ng: { show: "(change_type.sells.$error.required || change_type.sells.$error.pattern) && submitted" } } Please choose one of the options above. - if @enterprise.sells == 'unspecified' %input.big_button{ type: 'submit', value: 'Next', ng: { click: "submit(change_type)" } } diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index b6de336025..21ff5e1f25 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -25,6 +25,13 @@ = render partial: "change_type_form" +- if @enterprise.confirmed_at.nil? + .alert-box + Please confirm the email address for + %strong= "#{@enterprise.name}." + We've sent an email to + %strong= "#{@enterprise.email}." + %a.close{ href: "#" } × - if !@enterprise.visible .alert-box %strong Hint: @@ -38,7 +45,7 @@ }); .row - .alpha.seven.columns.dashboard_item + .alpha.seven.columns.dashboard_item#map .header %h3 %span.icon-map-marker @@ -54,7 +61,7 @@ .two.columns   - .seven.columns.omega.dashboard_item + .seven.columns.omega.dashboard_item#edit .header %h3 %span.icon-edit @@ -67,8 +74,8 @@ %span.icon-arrow-right .row - .seven.columns.alpha.dashboard_item - - if can? :admin, Spree::Product + - if can? :admin, Spree::Product + .seven.columns.alpha.dashboard_item#products .header %h3 %span.icon-th-large @@ -81,8 +88,8 @@ .two.columns   - .seven.columns.omega.dashboard_item - - if can? :admin, OrderCycle + - if can? :admin, OrderCycle + .seven.columns.omega.dashboard_item#order_cycles .header %h3 %span.icon-shopping-cart diff --git a/spec/features/admin/overview_spec.rb b/spec/features/admin/overview_spec.rb index 8f7063d47e..590c8528e8 100644 --- a/spec/features/admin/overview_spec.rb +++ b/spec/features/admin/overview_spec.rb @@ -28,6 +28,56 @@ feature %q{ end end + context "with an enterprise" do + let(:d1) { create(:distributor_enterprise) } + + before :each do + @enterprise_user.enterprise_roles.build(enterprise: d1).save + end + + it "displays a link to the map page" do + visit '/admin' + page.should have_selector ".dashboard_item h3", text: "Your profile live" + page.should have_selector ".dashboard_item .button.bottom", text: "SEE #{d1.name.upcase} LIVE" + end + + context "when enterprise has not been confirmed" do + before do + d1.confirmed_at = nil + d1.save! + end + + it "displays a message telling to user to confirm" do + visit '/admin' + page.should have_selector ".alert-box", text: "Please confirm the email address for #{d1.name}. We've sent an email to #{d1.email}." + end + end + + context "when visibilty is set to false" do + before do + d1.visible = false + d1.save! + end + + it "displays a message telling how to set visibility" do + visit '/admin' + page.should have_selector ".alert-box", text: "To allow people to find you, turn on your visibility under Manage #{d1.name}." + end + end + + pending "when user is a profile only" do + before do + d1.sells = "none" + d1.save! + end + + it "does not show a products item" do + visit '/admin' + page.should_not have_selector "#products" + end + end + end + context "with multiple enterprises" do let(:d1) { create(:distributor_enterprise) } let(:d2) { create(:distributor_enterprise) } diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index dd42b73e01..e4a98872af 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -38,9 +38,29 @@ module Spree user.enterprise_roles.create! enterprise: enterprise_none end - it { subject.can_manage_products?(user).should be_true } - it { subject.can_manage_enterprises?(user).should be_true } - it { subject.can_manage_orders?(user).should be_false } + context "as a non profile" do + before do + enterprise_none.is_primary_producer = true + enterprise_none.producer_profile_only = false + enterprise_none.save! + end + + it { subject.can_manage_products?(user).should be_true } + it { subject.can_manage_enterprises?(user).should be_true } + it { subject.can_manage_orders?(user).should be_false } + end + + context "as a profile" do + before do + enterprise_none.is_primary_producer = true + enterprise_none.producer_profile_only = true + enterprise_none.save! + end + + it { subject.can_manage_products?(user).should be_false } + it { subject.can_manage_enterprises?(user).should be_true } + it { subject.can_manage_orders?(user).should be_false } + end end context "as a new user with no enterprises" do From 928e5dc47478cfb5a0debcda952000c451e3e8ae Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 24 Oct 2014 16:39:59 +1100 Subject: [PATCH 124/681] Make styling more specific to single enterprise dashboard user only --- .../stylesheets/admin/dashboard-single-ent.css.sass | 2 +- .../admin/overview/single_enterprise_dashboard.html.haml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass index 67f9a3e603..4750377536 100644 --- a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass +++ b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass @@ -16,7 +16,7 @@ float: right -.dashboard_item +.dashboard_item.single-ent .header padding: 0.77778em 1.33333em 0.77778em 0.77778em height: auto !important diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 21ff5e1f25..e88fe4eb11 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -45,7 +45,7 @@ }); .row - .alpha.seven.columns.dashboard_item#map + .alpha.seven.columns.dashboard_item.single-ent#map .header %h3 %span.icon-map-marker @@ -61,7 +61,7 @@ .two.columns   - .seven.columns.omega.dashboard_item#edit + .seven.columns.omega.dashboard_item.single-ent#edit .header %h3 %span.icon-edit @@ -75,7 +75,7 @@ .row - if can? :admin, Spree::Product - .seven.columns.alpha.dashboard_item#products + .seven.columns.alpha.dashboard_item.single-ent#products .header %h3 %span.icon-th-large @@ -89,7 +89,7 @@   - if can? :admin, OrderCycle - .seven.columns.omega.dashboard_item#order_cycles + .seven.columns.omega.dashboard_item.single-ent#order_cycles .header %h3 %span.icon-shopping-cart From 8385bff55ee300e7a989e006bdb6cfc74dfd2810 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 17:59:58 +1100 Subject: [PATCH 125/681] Make admins get multi enterprise dashboard --- app/controllers/spree/admin/overview_controller_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index c73073c0fc..6eea9c6ea4 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -5,7 +5,7 @@ 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 - if OpenFoodNetwork::Permissions.new(spree_current_user).manages_one_enterprise? + if OpenFoodNetwork::Permissions.new(spree_current_user).manages_one_enterprise? && !spree_current_user.admin? @enterprise = @enterprises.first if @enterprise.sells == "unspecified" render "welcome", layout: "spree/layouts/bare_admin" From 6155600cb282309cd4b4e5079110583f4120b550 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 24 Oct 2014 18:15:48 +1100 Subject: [PATCH 126/681] Set environment for payment methods unless admin --- .../spree/admin/payment_methods_controller_decorator.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index 7941deb9ee..963b61fa81 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -1,6 +1,7 @@ module Spree module Admin PaymentMethodsController.class_eval do + before_filter :force_environment, only: [:create, :update] skip_before_filter :load_resource, only: [:show_provider_preferences] before_filter :load_hubs, only: [:new, :edit, :update] create.before :load_hubs @@ -46,6 +47,10 @@ module Spree private + def force_environment + params[:payment_method][:environment] = Rails.env unless spree_current_user.admin? + end + def load_data if spree_current_user.admin? || Rails.env.test? @providers = Gateway.providers.sort{|p1, p2| p1.name <=> p2.name } From 6f1c90ea25d61b632dcfea6c93c67f0da035d17c Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 25 Oct 2014 17:09:23 +1100 Subject: [PATCH 127/681] add toxons to prodcut reports --- lib/open_food_network/products_and_inventory_report.rb | 2 ++ .../lib/open_food_network/products_and_inventory_report_spec.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb index bd8e80ca0c..dbf2b3db73 100644 --- a/lib/open_food_network/products_and_inventory_report.rb +++ b/lib/open_food_network/products_and_inventory_report.rb @@ -13,6 +13,7 @@ module OpenFoodNetwork "Producer Suburb", "Product", "Product Properties", + "Taxon", "Variant Value", "Price", "Group Buy Unit Quantity", @@ -26,6 +27,7 @@ module OpenFoodNetwork variant.product.supplier.address.city, variant.product.name, variant.product.properties.map(&:name).join(", "), + variant.product.taxons.map(&:name).join(", "), variant.full_name, variant.price, variant.product.group_buy_unit_size, diff --git a/spec/lib/open_food_network/products_and_inventory_report_spec.rb b/spec/lib/open_food_network/products_and_inventory_report_spec.rb index 39328b87e7..3fd5248a50 100644 --- a/spec/lib/open_food_network/products_and_inventory_report_spec.rb +++ b/spec/lib/open_food_network/products_and_inventory_report_spec.rb @@ -18,6 +18,7 @@ module OpenFoodNetwork "Producer Suburb", "Product", "Product Properties", + "Taxon", "Variant Value", "Price", "Group Buy Unit Quantity", @@ -34,6 +35,7 @@ module OpenFoodNetwork variant.stub_chain(:product, :supplier, :address, :city).and_return("A city") variant.stub_chain(:product, :name).and_return("Product Name") variant.stub_chain(:product, :properties).and_return [double(name: "test"), double(name: "foo")] + variant.stub_chain(:product, :taxons).and_return [double(name: "test"), double(name: "foo")] variant.stub_chain(:product, :group_buy_unit_size).and_return(21) subject.stub(:variants).and_return [variant] From feff4fd46ff148b863bfa4bfb2431f0e2257885e Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 25 Oct 2014 18:51:03 +1100 Subject: [PATCH 128/681] fix reports taxons and specs --- lib/open_food_network/products_and_inventory_report.rb | 2 +- spec/features/admin/reports_spec.rb | 8 ++++---- .../products_and_inventory_report_spec.rb | 9 +++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb index dbf2b3db73..487ccfa67d 100644 --- a/lib/open_food_network/products_and_inventory_report.rb +++ b/lib/open_food_network/products_and_inventory_report.rb @@ -13,7 +13,7 @@ module OpenFoodNetwork "Producer Suburb", "Product", "Product Properties", - "Taxon", + "Taxons", "Variant Value", "Price", "Group Buy Unit Quantity", diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 5228b4c8a0..a1f1bcfec3 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -157,10 +157,10 @@ feature %q{ table = rows.map { |r| r.all("th,td").map { |c| c.text.strip } } 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(", "), "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, ""] + ["Supplier", "Producer Suburb", "Product", "Product Properties", "Taxons", "Variant Value", "Price", "Group Buy Unit Quantity", "Amount"], + [product_1.supplier.name, product_1.supplier.address.city, "Product Name", product_1.properties.join(", "), product_1.primary_taxon.name, "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(", "), product_1.primary_taxon.name, "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(", "), product_2.primary_taxon.name, "", "99.0", product_1.group_buy_unit_size.to_s, ""] ].sort end end diff --git a/spec/lib/open_food_network/products_and_inventory_report_spec.rb b/spec/lib/open_food_network/products_and_inventory_report_spec.rb index 3fd5248a50..9e449219e6 100644 --- a/spec/lib/open_food_network/products_and_inventory_report_spec.rb +++ b/spec/lib/open_food_network/products_and_inventory_report_spec.rb @@ -18,7 +18,7 @@ module OpenFoodNetwork "Producer Suburb", "Product", "Product Properties", - "Taxon", + "Taxons", "Variant Value", "Price", "Group Buy Unit Quantity", @@ -34,8 +34,8 @@ module OpenFoodNetwork variant.stub_chain(:product, :supplier, :name).and_return("Supplier") variant.stub_chain(:product, :supplier, :address, :city).and_return("A city") variant.stub_chain(:product, :name).and_return("Product Name") - variant.stub_chain(:product, :properties).and_return [double(name: "test"), double(name: "foo")] - variant.stub_chain(:product, :taxons).and_return [double(name: "test"), double(name: "foo")] + variant.stub_chain(:product, :properties).and_return [double(name: "property1"), double(name: "property2")] + variant.stub_chain(:product, :taxons).and_return [double(name: "taxon1"), double(name: "taxon2")] variant.stub_chain(:product, :group_buy_unit_size).and_return(21) subject.stub(:variants).and_return [variant] @@ -43,7 +43,8 @@ module OpenFoodNetwork "Supplier", "A city", "Product Name", - "test, foo", + "property1, property2", + "taxon1, taxon2", "Variant Name", 100, 21, From 68073d7239b9d905abc30c5ebe88ba2b0c7f3d54 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sat, 25 Oct 2014 19:03:56 +1100 Subject: [PATCH 129/681] revome config sidebar from index --- .../shipping_methods/index/remove_configuration_sidebar.deface | 1 + 1 file changed, 1 insertion(+) create mode 100644 app/overrides/spree/admin/shipping_methods/index/remove_configuration_sidebar.deface diff --git a/app/overrides/spree/admin/shipping_methods/index/remove_configuration_sidebar.deface b/app/overrides/spree/admin/shipping_methods/index/remove_configuration_sidebar.deface new file mode 100644 index 0000000000..cc3fbcdee1 --- /dev/null +++ b/app/overrides/spree/admin/shipping_methods/index/remove_configuration_sidebar.deface @@ -0,0 +1 @@ +remove "code[erb-loud]:contains(\"render :partial => 'spree/admin/shared/configuration_menu'\")" \ No newline at end of file From 43d3955627ea146b7e1d58c48d67308831fcaf9c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sat, 25 Oct 2014 19:56:52 +1100 Subject: [PATCH 130/681] Change wording of trial option on change_type_form --- app/views/spree/admin/overview/_change_type_form.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml index 13539474d5..530d870307 100644 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ b/app/views/spree/admin/overview/_change_type_form.html.haml @@ -34,7 +34,10 @@ %p Through an OFN shopfront .bottom 30 DAY TRIAL %p.description - Test out having your own shopfront with full access to all Shopfront features for 30 days. After your trial expires you can keep your Shopfront for a subscription cost of $50 per month. + Test out having your own shopfront with full access to all Shopfront features for 30 days. + %br + %br + At the end of your trial, there is a one-off $200 fee to fully activate your account. Then you will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). - else .shop_profile.option.one-third.column.alpha From f1e27a3ac340425f8029714389438d65688ca5a3 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 11:29:42 +1100 Subject: [PATCH 131/681] No need to import typography into this file, add note for future devs --- app/assets/stylesheets/darkswarm/mixins.sass | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/mixins.sass b/app/assets/stylesheets/darkswarm/mixins.sass index 6f4cb65440..00aa601b2f 100644 --- a/app/assets/stylesheets/darkswarm/mixins.sass +++ b/app/assets/stylesheets/darkswarm/mixins.sass @@ -1,4 +1,5 @@ -@import typography +// Note this mixin file is used in ADMIN and FRONTEND + @import branding From 64f2d1b34ec4a1c22692034f9aa9cccfa0bc7326 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 11:30:03 +1100 Subject: [PATCH 132/681] Remove unnecessary partial import --- app/assets/stylesheets/admin/dashboard-single-ent.css.sass | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass index 4750377536..07e8ac69c7 100644 --- a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass +++ b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass @@ -1,7 +1,5 @@ -@import ../darkswarm/branding @import ../darkswarm/mixins - .alert-box display: block background-color: #eff5dc From fd8eecf745162ac601dd866cf5272211e285931a Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 11:30:21 +1100 Subject: [PATCH 133/681] Remove unnecessary partial import --- app/assets/stylesheets/admin/welcome.css.sass | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/admin/welcome.css.sass b/app/assets/stylesheets/admin/welcome.css.sass index c7d084766f..5d14fc1436 100644 --- a/app/assets/stylesheets/admin/welcome.css.sass +++ b/app/assets/stylesheets/admin/welcome.css.sass @@ -1,4 +1,3 @@ -@import ../darkswarm/branding @import ../darkswarm/mixins #welcome_page From 977c5088334fe43219d9043ff7e8e2094a4b9f25 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 30 Oct 2014 12:25:50 +1100 Subject: [PATCH 134/681] Make enterprise limit error messages more specific --- app/models/enterprise.rb | 2 +- app/models/spree/user_decorator.rb | 2 +- spec/models/enterprise_spec.rb | 2 +- spec/models/spree/user_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 3fc89aae14..3f98e01eb4 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -301,7 +301,7 @@ class Enterprise < ActiveRecord::Base def enforce_ownership_limit unless owner.can_own_more_enterprises? - errors.add(:owner, "^You are not permitted to own own any more enterprises (limit is #{owner.enterprise_limit}).") + errors.add(:owner, "^#{owner.email} is not permitted to own any more enterprises (limit is #{owner.enterprise_limit}).") end end end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 06a413b469..4f119ab81b 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -31,7 +31,7 @@ Spree.user_class.class_eval do def limit_owned_enterprises if owned_enterprises.size > enterprise_limit - errors.add(:owned_enterprises, "^The nominated user is not permitted to own own any more enterprises (limit is #{enterprise_limit}).") + errors.add(:owned_enterprises, "^#{email} is not permitted to own any more enterprises (limit is #{enterprise_limit}).") end end end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 7ace25917a..31c7af9f7c 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -96,7 +96,7 @@ describe Enterprise do expect{ e2.owner = u1 e2.save! - }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: You are not permitted to own own any more enterprises (limit is 1)." + }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: #{u1.email} is not permitted to own any more enterprises (limit is 1)." end end end diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index f049c64b2f..e1a9239c95 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -19,7 +19,7 @@ describe Spree.user_class do expect { u2.owned_enterprises << e2 u2.save! - }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: The nominated user is not permitted to own own any more enterprises (limit is 1)." + }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: #{u2.email} is not permitted to own any more enterprises (limit is 1)." end end end From 65d13e049072f5f88aadd9f98a3e4fdcc65e7693 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 30 Oct 2014 12:29:14 +1100 Subject: [PATCH 135/681] Show error messages on enterprise index page --- .../admin/enterprises_controller.rb | 1 + app/models/model_set.rb | 5 +- app/views/admin/enterprises/index.html.haml | 2 + spec/features/admin/enterprises_spec.rb | 77 ++++++++++++++----- 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index b3fad9dc5c..b8dd9b9bd5 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -47,6 +47,7 @@ module Admin flash[:success] = 'Enterprises updated successfully' redirect_to main_app.admin_enterprises_path else + flash[:error] = 'Update failed' render :index end end diff --git a/app/models/model_set.rb b/app/models/model_set.rb index 0452b0bafb..d4760884b5 100644 --- a/app/models/model_set.rb +++ b/app/models/model_set.rb @@ -26,7 +26,10 @@ class ModelSet end def errors - @collection.map { |ef| ef.errors.full_messages }.flatten + errors = ActiveModel::Errors.new self + full_messages = @collection.map { |ef| ef.errors.full_messages }.flatten + full_messages.each { |fm| errors.add(:base, fm) } + errors end def save diff --git a/app/views/admin/enterprises/index.html.haml b/app/views/admin/enterprises/index.html.haml index ee1bdde996..64b8423baa 100644 --- a/app/views/admin/enterprises/index.html.haml +++ b/app/views/admin/enterprises/index.html.haml @@ -8,6 +8,8 @@ = render 'admin/shared/enterprises_sub_menu' += render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise_set } + = form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path do |f| %table#listing_enterprises.index %colgroup diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 06b1fd31a6..ae93ac19ea 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -35,28 +35,63 @@ feature %q{ end end - scenario "editing enterprises in bulk" do - s = create(:supplier_enterprise) - d = create(:distributor_enterprise, sells: 'none') - d_manager = create_enterprise_user - d_manager.enterprise_roles.build(enterprise: d).save - expect(d.owner).to_not eq d_manager + context "editing enterprises in bulk" do + let!(:s){ create(:supplier_enterprise) } + let!(:d){ create(:distributor_enterprise, sells: 'none') } + let!(:d_manager) { create_enterprise_user(enterprise_limit: 1) } - login_to_admin_section - click_link 'Enterprises' - - within("tr.enterprise-#{d.id}") do - expect(page).to have_checked_field "enterprise_set_collection_attributes_0_visible" - uncheck "enterprise_set_collection_attributes_0_visible" - select 'any', from: "enterprise_set_collection_attributes_0_sells" - select d_manager.email, from: 'enterprise_set_collection_attributes_0_owner_id' + before do + d_manager.enterprise_roles.build(enterprise: d).save + expect(d.owner).to_not eq d_manager + end + + context "without violating rules" do + before do + login_to_admin_section + click_link 'Enterprises' + end + + it "updates the enterprises" do + within("tr.enterprise-#{d.id}") do + expect(page).to have_checked_field "enterprise_set_collection_attributes_0_visible" + uncheck "enterprise_set_collection_attributes_0_visible" + select 'any', from: "enterprise_set_collection_attributes_0_sells" + select d_manager.email, from: 'enterprise_set_collection_attributes_0_owner_id' + end + click_button "Update" + flash_message.should == 'Enterprises updated successfully' + distributor = Enterprise.find(d.id) + expect(distributor.visible).to eq false + expect(distributor.sells).to eq 'any' + expect(distributor.owner).to eq d_manager + end + end + + context "with data that violates rules" do + let!(:second_distributor) { create(:distributor_enterprise, sells: 'none') } + + before do + d_manager.enterprise_roles.build(enterprise: second_distributor).save + expect(d.owner).to_not eq d_manager + + login_to_admin_section + click_link 'Enterprises' + end + + it "does not update the enterprises and displays errors" do + within("tr.enterprise-#{d.id}") do + select d_manager.email, from: 'enterprise_set_collection_attributes_0_owner_id' + end + within("tr.enterprise-#{second_distributor.id}") do + select d_manager.email, from: 'enterprise_set_collection_attributes_1_owner_id' + end + click_button "Update" + flash_message.should == 'Update failed' + expect(page).to have_content "#{d_manager.email} is not permitted to own any more enterprises (limit is 1)." + second_distributor.reload + expect(second_distributor.owner).to_not eq d_manager + end end - click_button "Update" - flash_message.should == 'Enterprises updated successfully' - distributor = Enterprise.find(d.id) - expect(distributor.visible).to eq false - expect(distributor.sells).to eq 'any' - expect(distributor.owner).to eq d_manager end scenario "viewing an enterprise" do @@ -350,7 +385,7 @@ feature %q{ # Then it should show me an error expect(page).to_not have_content 'Enterprise "zzz" has been successfully created!' - expect(page).to have_content "You are not permitted to own own any more enterprises (limit is 1)." + expect(page).to have_content "#{enterprise_user.email} is not permitted to own any more enterprises (limit is 1)." end end end From 6596e14e5d146da6bd177a37b442525275253c23 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 30 Oct 2014 12:54:11 +1100 Subject: [PATCH 136/681] Use shared partial for errors on EnterpriseFeeSet --- app/views/admin/enterprise_fees/index.html.haml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/views/admin/enterprise_fees/index.html.haml b/app/views/admin/enterprise_fees/index.html.haml index 48517c5931..ba3c3e6f4c 100644 --- a/app/views/admin/enterprise_fees/index.html.haml +++ b/app/views/admin/enterprise_fees/index.html.haml @@ -3,11 +3,7 @@ = ng_form_for @enterprise_fee_set, :url => main_app.bulk_update_admin_enterprise_fees_path, :html => {'ng-app' => 'enterprise_fees', 'ng-controller' => 'AdminEnterpriseFeesCtrl'} do |enterprise_fee_set_form| = hidden_field_tag 'enterprise_id', @enterprise.id if @enterprise - - if @enterprise_fee_set.errors.present? - %h2 Errors - %ul - - @enterprise_fee_set.errors.each do |error| - %li= error + = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise_fee_set } %input.search{'ng-model' => 'query', 'placeholder' => 'Search'} From 6329db23d7537a87ad4bb5a9c13303fc531b63c4 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 12:42:42 +1100 Subject: [PATCH 137/681] Remove styling for capitalisation for shopfront page --- app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index c2aadf5a46..1711779cf4 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -37,7 +37,6 @@ // Variant name .variant-name - text-transform: capitalize padding-left: 7.9375rem @media all and (max-width: 768px) padding-left: 4.9375rem @@ -94,7 +93,6 @@ padding-bottom: 0.65rem .summary-header - text-transform: capitalize padding-left: 7.9375rem @media all and (max-width: 768px) padding-left: 4.9375rem From 32daa1745cec10385142ab123daa90ad9df1beb4 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 15:03:03 +1100 Subject: [PATCH 138/681] Tweak logo for high density screens, make pretty and easier to read --- app/assets/images/open-food-network-beta.png | Bin 0 -> 15759 bytes app/assets/images/open-food-network-beta.svg | 840 +++++++++++++++++++ 2 files changed, 840 insertions(+) create mode 100644 app/assets/images/open-food-network-beta.png create mode 100644 app/assets/images/open-food-network-beta.svg diff --git a/app/assets/images/open-food-network-beta.png b/app/assets/images/open-food-network-beta.png new file mode 100644 index 0000000000000000000000000000000000000000..965a248162674593d6126008e88b2e2e186a4912 GIT binary patch literal 15759 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 bSFZH5SUx6Hq2u32X%tmuO{IF!hw%RcQI+sR literal 0 HcmV?d00001 diff --git a/app/assets/images/open-food-network-beta.svg b/app/assets/images/open-food-network-beta.svg new file mode 100644 index 0000000000..eb882701d4 --- /dev/null +++ b/app/assets/images/open-food-network-beta.svg @@ -0,0 +1,840 @@ + + + + + + + + BETA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BETA + + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 799c894bd33bd8678d0719a3128dde9c35fc83c9 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 15:03:36 +1100 Subject: [PATCH 139/681] Tweak logo embedding to use new version, and use srcset with src as a fallback --- app/views/home/index.html.haml | 3 ++- app/views/shared/menu/_large_menu.html.haml | 4 +++- app/views/shared/menu/_mobile_menu.html.haml | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 36ee67998f..da6dc97b02 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,7 +1,8 @@ #tagline .row .small-12.text-center.columns - %h1= image_tag "ofn_logo_beta.png", title: "Open Food Network (beta)" + %h1 + %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "550", height: "134", title: "Open Food Network (beta)"} %h2 An open marketplace that makes it easy to find, buy, sell and move sustainable local food. %ofn-modal{title: "Learn more", "ng-cloak" => true} diff --git a/app/views/shared/menu/_large_menu.html.haml b/app/views/shared/menu/_large_menu.html.haml index 71f297dede..a12d0030e0 100644 --- a/app/views/shared/menu/_large_menu.html.haml +++ b/app/views/shared/menu/_large_menu.html.haml @@ -1,7 +1,9 @@ %nav.top-bar.show-for-large-up{'data-topbar' => true} %section.top-bar-section %ul.left{} - %li.ofn-logo= link_to image_tag("ofn_logo_small.png"), root_path + %li.ofn-logo + %a{href: root_path} + %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"} %li.divider - if current_page? root_path %li diff --git a/app/views/shared/menu/_mobile_menu.html.haml b/app/views/shared/menu/_mobile_menu.html.haml index 6cc81cab9d..5ed57917fd 100644 --- a/app/views/shared/menu/_mobile_menu.html.haml +++ b/app/views/shared/menu/_mobile_menu.html.haml @@ -10,7 +10,9 @@ %aside.left-off-canvas-menu.show-for-medium-down %ul.off-canvas-list - %li= link_to image_tag("ofn_logo_small.png"), root_path + %li.ofn-logo + %a{href: root_path} + %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"} - if current_page? root_path %li.li-menu From 38e6575781a5d4df7772ade388da183c7d9a8d7e Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 15:46:32 +1100 Subject: [PATCH 140/681] Move first and last name fields to top of shipping address accordion --- app/views/checkout/_shipping.html.haml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index 5ba5ed15e4..b535908c53 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -51,9 +51,14 @@ .small-12.columns #ship_address{"ng-if" => "Checkout.requireShipAddress()"} %div.visible{"ng-if" => "!Checkout.ship_address_same_as_billing"} + .row + .small-6.columns + = validated_input "First Name", "order.ship_address.firstname", "ofn-focus" => "accordion['shipping']" + .small-6.columns + = validated_input "Last Name", "order.ship_address.lastname" .row .small-12.columns - = validated_input "Address", "order.ship_address.address1", "ofn-focus" => "accordion['shipping']" + = validated_input "Address", "order.ship_address.address1" .row .small-12.columns = validated_input "Address (contd.)", "order.ship_address.address2", required: false @@ -69,11 +74,7 @@ .small-6.columns.right = sa.select :country_id, available_countries.map{|c|[c.name, c.id]}, "ng-model" => "order.ship_address.country_id", required: true - .row - .small-6.columns - = validated_input "First Name", "order.ship_address.firstname" - .small-6.columns - = validated_input "Last Name", "order.ship_address.lastname" + .row .small-6.columns = validated_input "Phone", "order.ship_address.phone" From a27a80547857ea910451f9a46b57b997423b69ed Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 15:47:09 +1100 Subject: [PATCH 141/681] Tweak styling so the accordion hover colors dont fight the validation colors --- app/assets/stylesheets/darkswarm/checkout.css.sass | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass index a592c5dc25..77feb9c44e 100644 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -1,5 +1,6 @@ @import mixins @import branding +@import animations checkout display: block @@ -56,9 +57,13 @@ checkout // Logic to swap out up / down accordion icons //Foundation overrides + dd > a + @include csstrans + background: $disabled-light !important + dd > a:hover - background: $clr-brick-ultra-light !important - color: $clr-brick + background: $disabled-v-dark !important + color: white dd span.accordion-up From d8933deee08d492246977846532f8698955d1a69 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 17:01:23 +1100 Subject: [PATCH 142/681] Styling shopfront tabs make things better and more responsive --- .../stylesheets/darkswarm/tabs.css.sass | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/tabs.css.sass b/app/assets/stylesheets/darkswarm/tabs.css.sass index a128394e75..0c911eb66a 100644 --- a/app/assets/stylesheets/darkswarm/tabs.css.sass +++ b/app/assets/stylesheets/darkswarm/tabs.css.sass @@ -30,34 +30,42 @@ border-color: $clr-brick-bright background-color: rgba(255, 255, 255, 0) - dl dd a - @include avenir - background: transparent - text-transform: uppercase - line-height: 50px - font-size: 0.875em - text-shadow: 0 -1px 1px #ffffff - &:hover, &:focus, &:active - color: $clr-brick-bright - @media all and (max-width: 768px) - line-height: 30px !important - border: none - &, &:hover - background: none - padding: 0px 2.2em - @media all and (max-width: 768px) - line-height: inherit !important + dl dd + + text-align: center + @media all and (max-width: 640px) + text-align: left + + a + @include avenir + background: transparent + text-transform: uppercase + line-height: 1 + font-size: 0.875em + text-shadow: 0 -1px 1px #ffffff + padding: 1em + border: none + &:hover, &:focus, &:active + color: $clr-brick-bright + &, &:hover + background: none + + @media all and (max-width: 640px) + padding: 0.35em 0 0.65em 0 + text-shadow: none // inactive nav link - dl dd border-top: 4px solid transparent + a:after padding-left: 8px - content: "\e634" + content: "\e633" visibility: hidden @include icon-font + @media all and (max-width: 640px) + content: "\e633" dd:hover a:after visibility: visible @@ -67,12 +75,19 @@ dl dd.active border-top: 4px solid $clr-brick + @media all and (max-width: 640px) + border-top: 4px solid transparent + background-color: $clr-brick a color: $clr-brick + @media all and (max-width: 640px) + color: white a:after - content: "\e633" + content: "\e634" visibility: visible @include icon-font + @media all and (max-width: 640px) + content: "\e633" // content revealed in accordion From 733b249b12ecc45d64f4552aed98894d5e22cabe Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 17:01:42 +1100 Subject: [PATCH 143/681] making tabs break down better for responsive and small devices --- app/views/shopping_shared/_tabs.html.haml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/views/shopping_shared/_tabs.html.haml b/app/views/shopping_shared/_tabs.html.haml index c19ec56755..d136efa045 100644 --- a/app/views/shopping_shared/_tabs.html.haml +++ b/app/views/shopping_shared/_tabs.html.haml @@ -1,14 +1,16 @@ #tabs{"ng-controller" => "TabsCtrl"} .row - %tabset.small-12.columns + %tabset -# Build all tabs. - - for name, heading in { about: "About #{current_distributor.name}", - producers: "Producers", - groups: "Groups", - contact: "Contact"} + - for name, heading_cols in { about: ["About #{current_distributor.name}", 4], + producers: ["Producers",3], + groups: ["Groups",2], + contact: ["Contact",3]} -# tabs take tab path in 'active' and 'select' functions defined in TabsCtrl. - %tab{heading: heading, + - heading, cols = heading_cols + %tab.columns{heading: heading, id: "tab_#{name}", active: "active(\'#{name}\')", - select: "select(\'#{name}\')"} + select: "select(\'#{name}\')", + class: "small-12 medium-#{cols}" } = render "shopping_shared/#{name}" From 34f47cedae99cf3510be43cde4874e3d0285e0af Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 30 Oct 2014 17:02:08 +1100 Subject: [PATCH 144/681] Fix markup for better layout and getting column padding sitting where it should --- app/views/shopping_shared/_about.html.haml | 13 ++++++----- app/views/shopping_shared/_groups.html.haml | 23 ++++++++++--------- .../shopping_shared/_producers.html.haml | 17 +++++++------- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app/views/shopping_shared/_about.html.haml b/app/views/shopping_shared/_about.html.haml index 0e71125f65..ffe66c6c82 100644 --- a/app/views/shopping_shared/_about.html.haml +++ b/app/views/shopping_shared/_about.html.haml @@ -1,7 +1,8 @@ .content#about{"ng-controller" => "AboutUsCtrl", bindonce: true} - .row - .small-12.large-8.columns.panel - %img.hero-img-small{"bo-src" => "CurrentHub.hub.promo_image", "bo-if" => "CurrentHub.hub.promo_image"} - %p{"bo-html" => "CurrentHub.hub.long_description"} - .small-12.large-4.columns -   + .panel + .row + .small-12.large-8.columns + %img.hero-img-small{"bo-src" => "CurrentHub.hub.promo_image", "bo-if" => "CurrentHub.hub.promo_image"} + %p{"bo-html" => "CurrentHub.hub.long_description"} + .small-12.large-4.columns +   diff --git a/app/views/shopping_shared/_groups.html.haml b/app/views/shopping_shared/_groups.html.haml index 7e6b48997e..68c68882fb 100644 --- a/app/views/shopping_shared/_groups.html.haml +++ b/app/views/shopping_shared/_groups.html.haml @@ -1,12 +1,13 @@ .content - .row - .small-12.columns.panel - - if current_distributor.groups.length > 0 - %h5 - =current_distributor.name - is part of: - %ul.bullet-list - - for group in current_distributor.groups - %li - %a{href: main_app.groups_path + "/#/#group#{group.id}"} - = group.name + .panel + .row + .small-12.large-4.columns + - if current_distributor.groups.length > 0 + %h5 + =current_distributor.name + is part of: + %ul.bullet-list + - for group in current_distributor.groups + %li + %a{href: main_app.groups_path + "/#/#group#{group.id}"} + = group.name diff --git a/app/views/shopping_shared/_producers.html.haml b/app/views/shopping_shared/_producers.html.haml index 25c0c0599d..d1ece47329 100644 --- a/app/views/shopping_shared/_producers.html.haml +++ b/app/views/shopping_shared/_producers.html.haml @@ -1,9 +1,10 @@ .content#producers{"ng-controller" => "ProducersTabCtrl"} - .row - .small-12.columns.panel - %h5 {{CurrentHub.hub.name}}'s producers: - %ul.small-block-grid-2.large-block-grid-4 - %li{"ng-repeat" => "enterprise in CurrentHub.hub.producers"} - %enterprise-modal - %i.ofn-i_036-producers - {{ enterprise.name }} + .panel + .row + .small-12.columns + %h5 {{CurrentHub.hub.name}}'s producers: + %ul.small-block-grid-2.large-block-grid-4 + %li{"ng-repeat" => "enterprise in CurrentHub.hub.producers"} + %enterprise-modal + %i.ofn-i_036-producers + {{ enterprise.name }} From 433806aca83b955cb37bead4afba110447136085 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 29 Oct 2014 14:01:12 +1100 Subject: [PATCH 145/681] Remove unused helpers and standardise naming --- app/helpers/order_cycles_helper.rb | 17 +----------- app/views/admin/order_cycles/_form.html.haml | 2 +- spec/helpers/order_cycles_helper_spec.rb | 29 ++++---------------- 3 files changed, 7 insertions(+), 41 deletions(-) diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 627d117368..4a865ec12a 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -11,7 +11,7 @@ module OrderCyclesHelper order_cycle_permitted_enterprises.is_primary_producer.by_name end - def coordinating_enterprises + def order_cycle_coordinating_enterprises order_cycle_hub_enterprises end @@ -19,16 +19,6 @@ module OrderCyclesHelper order_cycle_permitted_enterprises.is_distributor.by_name end - def order_cycle_local_remote_class(distributor, order_cycle) - if distributor.nil? || order_cycle.nil? - '' - elsif order_cycle.distributors.include? distributor - ' local' - else - ' remote' - end - end - def order_cycle_status_class(order_cycle) if order_cycle.undated? 'undated' @@ -42,11 +32,6 @@ module OrderCyclesHelper end - def distributor_options(distributors, current_distributor, order_cycle) - options = distributors.map { |d| [d.name, d.id, {:class => order_cycle_local_remote_class(d, order_cycle).strip}] } - options_for_select(options, current_distributor) - end - def order_cycle_options @order_cycles. with_distributor(current_distributor). diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index 82146fabdd..b7e16e0128 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -21,7 +21,7 @@ %h2 Coordinator = f.label :coordinator_id, 'Coordinator' -= f.collection_select :coordinator_id, coordinating_enterprises, :id, :name, {include_blank: true}, {'ng-model' => 'order_cycle.coordinator_id', 'ofn-on-change' => 'order_cycle.coordinator_fees = []', 'required' => true} += f.collection_select :coordinator_id, order_cycle_coordinating_enterprises, :id, :name, {include_blank: true}, {'ng-model' => 'order_cycle.coordinator_id', 'ofn-on-change' => 'order_cycle.coordinator_fees = []', 'required' => true} = render 'coordinator_fees', f: f diff --git a/spec/helpers/order_cycles_helper_spec.rb b/spec/helpers/order_cycles_helper_spec.rb index 3717f34462..e421499fe9 100644 --- a/spec/helpers/order_cycles_helper_spec.rb +++ b/spec/helpers/order_cycles_helper_spec.rb @@ -1,28 +1,8 @@ require 'spec_helper' describe OrderCyclesHelper do - describe "generating local/remote classes for order cycle selection" do - it "returns blank when no distributor or order cycle is selected" do - helper.order_cycle_local_remote_class(nil, double(:order_cycle)).should == '' - helper.order_cycle_local_remote_class(double(:distributor), nil).should == '' - end - - it "returns local when the order cycle includes the current distributor" do - distributor = double(:enterprise) - order_cycle = double(:order_cycle, distributors: [distributor]) - - helper.order_cycle_local_remote_class(distributor, order_cycle).should == ' local' - end - - it "returns remote when the order cycle does not include the current distributor" do - distributor = double(:enterprise) - order_cycle = double(:order_cycle, distributors: []) - - helper.order_cycle_local_remote_class(distributor, order_cycle).should == ' remote' - end - end - - it "gives me the pickup time for an order_cycle" do + describe "pickup time" do + it "gives me the pickup time for the current order cycle" do d = create(:distributor_enterprise, name: 'Green Grass') oc1 = create(:simple_order_cycle, name: 'oc 1', distributors: [d]) exchange = Exchange.find(oc1.exchanges.to_enterprises(d).outgoing.first.id) @@ -31,9 +11,9 @@ describe OrderCyclesHelper do helper.stub(:current_order_cycle).and_return oc1 helper.stub(:current_distributor).and_return d helper.pickup_time.should == "turtles" - end + end - it "should give me the pickup time for any order cycle" do + it "gives me the pickup time for any order cycle" do d = create(:distributor_enterprise, name: 'Green Grass') oc1 = create(:simple_order_cycle, name: 'oc 1', distributors: [d]) oc2= create(:simple_order_cycle, name: 'oc 1', distributors: [d]) @@ -44,5 +24,6 @@ describe OrderCyclesHelper do helper.stub(:current_order_cycle).and_return oc1 helper.stub(:current_distributor).and_return d helper.pickup_time(oc2).should == "turtles" + end end end From fc1c3abb9ffcf97ce4692b79bf57635731ce101d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 29 Oct 2014 14:01:32 +1100 Subject: [PATCH 146/681] Remove duplicate data injection (duplicated in darkswarm layout) --- app/views/checkout/_form.html.haml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index bcbcb615fc..91153bdb4b 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -6,7 +6,6 @@ = inject_available_shipping_methods = inject_available_payment_methods - = inject_current_order = render partial: "checkout/details", locals: {f: f} = render partial: "checkout/billing", locals: {f: f} From 1d9a3f33e0c31d7d4c03f77c84338e7c343d723b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 29 Oct 2014 15:26:34 +1100 Subject: [PATCH 147/681] Order cycle distributors must have shipping and payment methods --- app/helpers/order_cycles_helper.rb | 21 +++++++++++++++++-- app/views/admin/order_cycles/_form.html.haml | 2 +- spec/features/admin/order_cycles_spec.rb | 10 +++++++-- spec/helpers/order_cycles_helper_spec.rb | 22 ++++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 4a865ec12a..579db4251e 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -12,11 +12,28 @@ module OrderCyclesHelper end def order_cycle_coordinating_enterprises - order_cycle_hub_enterprises + order_cycle_permitted_enterprises.is_distributor.by_name end def order_cycle_hub_enterprises - order_cycle_permitted_enterprises.is_distributor.by_name + enterprises = order_cycle_permitted_enterprises.is_distributor.by_name + + enterprises.map do |e| + disabled_message = nil + if e.shipping_methods.empty? && e.payment_methods.empty? + disabled_message = 'no shipping or payment methods' + elsif e.shipping_methods.empty? + disabled_message = 'no shipping methods' + elsif e.payment_methods.empty? + disabled_message = 'no payment methods' + end + + if disabled_message + ["#{e.name} (#{disabled_message})", e.id, {disabled: true}] + else + [e.name, e.id] + end + end end def order_cycle_status_class(order_cycle) diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index b7e16e0128..9aaac195ee 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -40,7 +40,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_distributed_products_form' -= select_tag :new_distributor_id, options_from_collection_for_select(order_cycle_hub_enterprises, :id, :name), {'ng-model' => 'new_distributor_id'} += select_tag :new_distributor_id, options_for_select(order_cycle_hub_enterprises), {'ng-model' => 'new_distributor_id'} = f.submit 'Add distributor', 'ng-click' => 'addDistributor($event)' .actions diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index c1aec54926..45f2ea115a 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -66,6 +66,9 @@ feature %q{ v1 = create(:variant, product: product) v2 = create(:variant, product: product) distributor = create(:distributor_enterprise, name: 'My distributor') + create(:shipping_method, distributors: [distributor]) + create(:payment_method, distributors: [distributor]) + # And some enterprise fees supplier_fee = create(:enterprise_fee, enterprise: supplier, name: 'Supplier fee') @@ -248,6 +251,8 @@ feature %q{ coordinator = create(:distributor_enterprise, name: 'My coordinator') supplier = create(:supplier_enterprise, name: 'My supplier') distributor = create(:distributor_enterprise, name: 'My distributor') + create(:shipping_method, distributors: [distributor]) + create(:payment_method, distributors: [distributor]) product = create(:product, supplier: supplier) v1 = create(:variant, product: product) v2 = create(:variant, product: product) @@ -443,6 +448,9 @@ feature %q{ let!(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Unmanaged Distributor') } let!(:distributor_permitted) { create(:distributor_enterprise, name: 'Permitted distributor') } let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor fee') } + let!(:shipping_method) { create(:shipping_method, distributors: [distributor_managed, distributor_unmanaged, distributor_permitted]) } + let!(:payment_method) { create(:payment_method, distributors: [distributor_managed, distributor_unmanaged, distributor_permitted]) } + let!(:supplier_permitted_relationship) do create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed, permissions_list: [:add_to_order_cycle]) @@ -588,8 +596,6 @@ feature %q{ let!(:v) { create(:variant, product: p3) } let!(:fee) { create(:enterprise_fee, enterprise: enterprise, name: 'Coord fee') } - use_short_wait - before do user.enterprise_roles.create! enterprise: enterprise login_to_admin_as user diff --git a/spec/helpers/order_cycles_helper_spec.rb b/spec/helpers/order_cycles_helper_spec.rb index e421499fe9..3003fd63cf 100644 --- a/spec/helpers/order_cycles_helper_spec.rb +++ b/spec/helpers/order_cycles_helper_spec.rb @@ -1,6 +1,28 @@ require 'spec_helper' describe OrderCyclesHelper do + describe "finding hub enterprises" do + let(:e) { create(:distributor_enterprise, name: 'enterprise') } + + before do + helper.stub(:order_cycle_permitted_enterprises) { Enterprise.where(id: e.id) } + end + + it "returns enterprises without shipping methods as disabled" do + create(:payment_method, distributors: [e]) + helper.order_cycle_hub_enterprises.should == [['enterprise (no shipping methods)', e.id, {disabled: true}]] + end + + it "returns enterprises without payment methods as disabled" do + create(:shipping_method, distributors: [e]) + helper.order_cycle_hub_enterprises.should == [['enterprise (no payment methods)', e.id, {disabled: true}]] + end + + it "returns enterprises with neither shipping nor payment methods as disabled" do + helper.order_cycle_hub_enterprises.should == [['enterprise (no shipping or payment methods)', e.id, {disabled: true}]] + end + end + describe "pickup time" do it "gives me the pickup time for the current order cycle" do d = create(:distributor_enterprise, name: 'Green Grass') From f0d3b987d4b8d7d239e8afcf7e4762396000174e Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 29 Oct 2014 16:57:18 +1100 Subject: [PATCH 148/681] Show hubs as unavailable when they do not have available payment and shipping methods --- app/controllers/base_controller.rb | 2 +- app/models/enterprise.rb | 8 +++++- app/models/spree/payment_method_decorator.rb | 7 ++++++ spec/models/enterprise_spec.rb | 26 ++++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index 2ac35fa3ad..88c0f89aec 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -13,6 +13,6 @@ class BaseController < ApplicationController before_filter :check_order_cycle_expiry def load_active_distributors - @active_distributors ||= Enterprise.distributors_with_active_order_cycles + @active_distributors ||= Enterprise.distributors_with_active_order_cycles.ready_for_checkout end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 3f98e01eb4..fc95d32613 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -65,7 +65,13 @@ class Enterprise < ActiveRecord::Base scope :visible, where(:visible => true) scope :confirmed, where('confirmed_at IS NOT NULL') scope :unconfirmed, where('confirmed_at IS NULL') - scope :activated, where('confirmed_at IS NOT NULL AND sells <> \'unspecified\'') + scope :activated, where("confirmed_at IS NOT NULL AND sells != 'unspecified'") + scope :ready_for_checkout, lambda { + joins(:shipping_methods). + joins(:payment_methods). + merge(Spree::PaymentMethod.available). + select('DISTINCT enterprises.*') + } scope :is_primary_producer, where(:is_primary_producer => true) scope :is_distributor, where('sells != ?', 'none') scope :supplying_variant_in, lambda { |variants| joins(:supplied_products => :variants_including_master).where('spree_variants.id IN (?)', variants).select('DISTINCT enterprises.*') } diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index 01f6e2f0e8..fd513cc0d3 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -24,6 +24,13 @@ Spree::PaymentMethod.class_eval do scope :by_name, order('spree_payment_methods.name ASC') + # Rewrite Spree's ruby-land class method as a scope + scope :available, lambda { |display_on='both'| + where(active: true). + where('spree_payment_methods.display_on=? OR spree_payment_methods.display_on=? OR spree_payment_methods.display_on IS NULL', display_on, ''). + where('spree_payment_methods.environment=? OR spree_payment_methods.environment=? OR spree_payment_methods.environment IS NULL', Rails.env, '') + } + def has_distributor?(distributor) self.distributors.include?(distributor) end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 31c7af9f7c..f6cc94b8ce 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -167,6 +167,32 @@ describe Enterprise do end end + describe "ready_for_checkout" do + let!(:e) { create(:enterprise) } + + it "does not show enterprises with no payment methods" do + create(:shipping_method, distributors: [e]) + Enterprise.ready_for_checkout.should_not include e + end + + it "does not show enterprises with no shipping methods" do + create(:payment_method, distributors: [e]) + Enterprise.ready_for_checkout.should_not include e + end + + it "does not show enterprises with unavailable payment methods" do + create(:shipping_method, distributors: [e]) + create(:payment_method, distributors: [e], active: false) + Enterprise.ready_for_checkout.should_not include e + end + + it "shows enterprises with available payment and shipping methods" do + create(:shipping_method, distributors: [e]) + create(:payment_method, distributors: [e]) + Enterprise.ready_for_checkout.should include e + end + end + describe "distributors_with_active_order_cycles" do it "finds active distributors by order cycles" do s = create(:supplier_enterprise) From 9f43146e47c746533ea6c6bb0d327d3ac9caf458 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 29 Oct 2014 17:01:08 +1100 Subject: [PATCH 149/681] Payment methods need to be available, too --- app/helpers/order_cycles_helper.rb | 4 ++-- spec/helpers/order_cycles_helper_spec.rb | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 579db4251e..2d230f19d2 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -20,11 +20,11 @@ module OrderCyclesHelper enterprises.map do |e| disabled_message = nil - if e.shipping_methods.empty? && e.payment_methods.empty? + if e.shipping_methods.empty? && e.payment_methods.available.empty? disabled_message = 'no shipping or payment methods' elsif e.shipping_methods.empty? disabled_message = 'no shipping methods' - elsif e.payment_methods.empty? + elsif e.payment_methods.available.empty? disabled_message = 'no payment methods' end diff --git a/spec/helpers/order_cycles_helper_spec.rb b/spec/helpers/order_cycles_helper_spec.rb index 3003fd63cf..c687463d59 100644 --- a/spec/helpers/order_cycles_helper_spec.rb +++ b/spec/helpers/order_cycles_helper_spec.rb @@ -18,6 +18,12 @@ describe OrderCyclesHelper do helper.order_cycle_hub_enterprises.should == [['enterprise (no payment methods)', e.id, {disabled: true}]] end + it "returns enterprises with unavailable payment methods as disabled" do + create(:shipping_method, distributors: [e]) + create(:payment_method, distributors: [e], active: false) + helper.order_cycle_hub_enterprises.should == [['enterprise (no payment methods)', e.id, {disabled: true}]] + end + it "returns enterprises with neither shipping nor payment methods as disabled" do helper.order_cycle_hub_enterprises.should == [['enterprise (no shipping or payment methods)', e.id, {disabled: true}]] end From 94d50f220f698f5bc9da880ceab9b684fb017ac0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 31 Oct 2014 11:59:46 +1100 Subject: [PATCH 150/681] Display an error message to admin when there are hubs in order cycles that are not ready for checkout --- .../spree/admin/base_controller_decorator.rb | 35 +++++++++++++++++++ app/models/enterprise.rb | 10 ++++++ .../spree/admin/base_controller_spec.rb | 23 ++++++++++++ spec/features/admin/order_cycles_spec.rb | 30 ++++++++++++++++ spec/models/enterprise_spec.rb | 26 ++++++++++++++ 5 files changed, 124 insertions(+) diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index 7199ca4dec..85904590c3 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -1,4 +1,16 @@ Spree::Admin::BaseController.class_eval do + before_filter :warn_invalid_order_cycles + + # Warn the user when they have an active order cycle with hubs that are not ready + # for checkout (ie. does not have valid shipping and payment methods). + def warn_invalid_order_cycles + distributors = active_distributors_not_ready_for_checkout + + if distributors.any? && flash[:notice].nil? + flash[:notice] = active_distributors_not_ready_for_checkout_message(distributors) + end + end + # Override Spree method # It's a shame Spree doesn't just let CanCan handle this in it's own way def authorize_admin @@ -23,4 +35,27 @@ Spree::Admin::BaseController.class_eval do redirect_to root_path(anchor: "login?after_login=#{request.env['PATH_INFO']}") end end + + + private + + def active_distributors_not_ready_for_checkout + ocs = OrderCycle.managed_by(spree_current_user).active + distributors = ocs.map(&:distributors).flatten.uniq + Enterprise.where('id IN (?)', distributors).not_ready_for_checkout + end + + def active_distributors_not_ready_for_checkout_message(distributors) + distributor_names = distributors.map(&:name).join ', ' + + if distributors.count > 1 + "The hubs #{distributor_names} are listed in an active order cycle, " + + "but do not have valid shipping and payment methods. " + + "Until you set these up, customers will not be able to shop at these hubs." + else + "The hub #{distributor_names} is listed in an active order cycle, " + + "but does not have valid shipping and payment methods. " + + "Until you set these up, customers will not be able to shop at this hub." + end + end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index fc95d32613..31ae2b3ee2 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -72,6 +72,16 @@ class Enterprise < ActiveRecord::Base merge(Spree::PaymentMethod.available). select('DISTINCT enterprises.*') } + scope :not_ready_for_checkout, lambda { + # When ready_for_checkout is empty, ActiveRecord generates the SQL: + # id NOT IN (NULL) + # I would have expected this to return all rows, but instead it returns none. To + # work around this, we use the "OR ?=0" clause to return all rows when there are + # no enterprises ready for checkout. + where('id NOT IN (?) OR ?=0', + Enterprise.ready_for_checkout, + Enterprise.ready_for_checkout.count) + } scope :is_primary_producer, where(:is_primary_producer => true) scope :is_distributor, where('sells != ?', 'none') scope :supplying_variant_in, lambda { |variants| joins(:supplied_products => :variants_including_master).where('spree_variants.id IN (?)', variants).select('DISTINCT enterprises.*') } diff --git a/spec/controllers/spree/admin/base_controller_spec.rb b/spec/controllers/spree/admin/base_controller_spec.rb index 0985d733a7..c56b3ae0db 100644 --- a/spec/controllers/spree/admin/base_controller_spec.rb +++ b/spec/controllers/spree/admin/base_controller_spec.rb @@ -12,4 +12,27 @@ describe Spree::Admin::BaseController do get :index response.should redirect_to root_path(anchor: "login?after_login=/anonymous") end + + describe "displaying error messages for active distributors not ready for checkout" do + it "generates an error message when there is one distributor" do + distributor = double(:distributor, name: 'My Hub') + controller. + send(:active_distributors_not_ready_for_checkout_message, [distributor]). + should == + "The hub My Hub is listed in an active order cycle, " + + "but does not have valid shipping and payment methods. " + + "Until you set these up, customers will not be able to shop at this hub." + end + + it "generates an error message when there are several distributors" do + d1 = double(:distributor, name: 'Hub One') + d2 = double(:distributor, name: 'Hub Two') + controller. + send(:active_distributors_not_ready_for_checkout_message, [d1, d2]). + should == + "The hubs Hub One, Hub Two are listed in an active order cycle, " + + "but do not have valid shipping and payment methods. " + + "Until you set these up, customers will not be able to shop at these hubs." + end + end end diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 45f2ea115a..e5d8769982 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -439,6 +439,36 @@ feature %q{ end + describe "ensuring that hubs in order cycles have valid shipping and payment methods" do + context "when they don't" do + let(:hub) { create(:distributor_enterprise) } + let!(:oc) { create(:simple_order_cycle, distributors: [hub]) } + + it "displays a warning on the dashboard" do + login_to_admin_section + page.should have_content "The hub #{hub.name} is listed in an active order cycle, but does not have valid shipping and payment methods. Until you set these up, customers will not be able to shop at this hub." + end + + it "displays a warning on the order cycles screen" do + login_to_admin_section + visit admin_order_cycles_path + page.should have_content "The hub #{hub.name} is listed in an active order cycle, but does not have valid shipping and payment methods. Until you set these up, customers will not be able to shop at this hub." + end + end + + context "when they do" do + let(:hub) { create(:distributor_enterprise) } + let!(:shipping_method) { create(:shipping_method, distributors: [hub]) } + let!(:payment_method) { create(:payment_method, distributors: [hub]) } + let!(:oc) { create(:simple_order_cycle, distributors: [hub]) } + + it "does not display the warning on the dashboard" do + login_to_admin_section + page.should_not have_content "does not have valid shipping and payment methods" + end + end + end + context "as an enterprise user" do let!(:supplier_managed) { create(:supplier_enterprise, name: 'Managed supplier') } diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index f6cc94b8ce..65571e3da7 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -193,6 +193,32 @@ describe Enterprise do end end + describe "not_ready_for_checkout" do + let!(:e) { create(:enterprise) } + + it "shows enterprises with no payment methods" do + create(:shipping_method, distributors: [e]) + Enterprise.not_ready_for_checkout.should include e + end + + it "shows enterprises with no shipping methods" do + create(:payment_method, distributors: [e]) + Enterprise.not_ready_for_checkout.should include e + end + + it "shows enterprises with unavailable payment methods" do + create(:shipping_method, distributors: [e]) + create(:payment_method, distributors: [e], active: false) + Enterprise.not_ready_for_checkout.should include e + end + + it "does not show enterprises with available payment and shipping methods" do + create(:shipping_method, distributors: [e]) + create(:payment_method, distributors: [e]) + Enterprise.not_ready_for_checkout.should_not include e + end + end + describe "distributors_with_active_order_cycles" do it "finds active distributors by order cycles" do s = create(:supplier_enterprise) From abeabd5b1c7e8ce978731a92ab60c868baf119b2 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 31 Oct 2014 14:04:43 +1100 Subject: [PATCH 151/681] Add Enterprise#ready_for_checkout? --- app/models/enterprise.rb | 4 ++++ spec/models/enterprise_spec.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 31ae2b3ee2..0840a53df3 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -277,6 +277,10 @@ class Enterprise < ActiveRecord::Base select('DISTINCT spree_taxons.*') end + def ready_for_checkout? + shipping_methods.any? && payment_methods.available.any? + end + def shop_trial_in_progress? !!shop_trial_start_date && (shop_trial_start_date + SHOP_TRIAL_LENGTH.days > Time.now) && diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 65571e3da7..4fa6e2b3f6 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -219,6 +219,32 @@ describe Enterprise do end end + describe "#ready_for_checkout?" do + let!(:e) { create(:enterprise) } + + it "returns false for enterprises with no payment methods" do + create(:shipping_method, distributors: [e]) + e.reload.should_not be_ready_for_checkout + end + + it "returns false for enterprises with no shipping methods" do + create(:payment_method, distributors: [e]) + e.reload.should_not be_ready_for_checkout + end + + it "returns false for enterprises with unavailable payment methods" do + create(:shipping_method, distributors: [e]) + create(:payment_method, distributors: [e], active: false) + e.reload.should_not be_ready_for_checkout + end + + it "returns true for enterprises with available payment and shipping methods" do + create(:shipping_method, distributors: [e]) + create(:payment_method, distributors: [e]) + e.reload.should be_ready_for_checkout + end + end + describe "distributors_with_active_order_cycles" do it "finds active distributors by order cycles" do s = create(:supplier_enterprise) From 7aefa05efd2c6c9b5a3200b66e78413531ca776d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 31 Oct 2014 14:48:08 +1100 Subject: [PATCH 152/681] Fix specs broken by requirement for valid shipping and payment method --- spec/features/consumer/home_spec.rb | 2 ++ spec/features/consumer/shopping/shopping_spec.rb | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/spec/features/consumer/home_spec.rb b/spec/features/consumer/home_spec.rb index dd39dc937d..56739187a0 100644 --- a/spec/features/consumer/home_spec.rb +++ b/spec/features/consumer/home_spec.rb @@ -5,6 +5,8 @@ feature 'Home', js: true do include UIComponentHelper let!(:distributor) { create(:distributor_enterprise) } + let!(:shipping_method) { create(:shipping_method, distributors: [distributor]) } + let!(:payment_method) { create(:payment_method, distributors: [distributor]) } let!(:invisible_distributor) { create(:distributor_enterprise, visible: false) } let(:d1) { create(:distributor_enterprise) } let(:d2) { create(:distributor_enterprise) } diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index d4db6bff29..b9b2a21219 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -10,6 +10,8 @@ feature "As a consumer I want to shop with a distributor", js: true do let(:distributor) { create(:distributor_enterprise) } let(:supplier) { create(:supplier_enterprise) } + let(:shipping_method) { create(:shipping_method, distributors: [distributor]) } + let(:payment_method) { create(:payment_method, distributors: [distributor]) } let(:oc1) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), orders_close_at: 2.days.from_now) } let(:oc2) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), orders_close_at: 3.days.from_now) } let(:product) { create(:simple_product, supplier: supplier) } @@ -64,6 +66,10 @@ feature "As a consumer I want to shop with a distributor", js: true do end it "shows products after selecting an order cycle" do + # Hubs cannot be selected without a valid shipping and payment method + shipping_method + payment_method + product.master.update_attribute(:display_name, "kitten") product.master.update_attribute(:display_as, "rabbit") exchange1.variants << product.master ## add product to exchange From 8488b8e4698347f77f6480fff53c0c9929116d8d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 31 Oct 2014 15:02:58 +1100 Subject: [PATCH 153/681] When user has selected a hub that is not ready for checkout, unselect it --- app/controllers/application_controller.rb | 9 ++++++ app/controllers/base_controller.rb | 1 + app/controllers/checkout_controller.rb | 1 + spec/controllers/base_controller_spec.rb | 36 +++++++++++++++++------ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index decc48ac7e..3b76ee3168 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -32,6 +32,15 @@ class ApplicationController < ActionController::Base end end + def check_hub_ready_for_checkout + if current_distributor && !current_distributor.ready_for_checkout? + current_order.empty! + current_order.set_distribution! nil, nil + flash[:info] = "The hub you have selected is temporarily closed for orders. Please try again later." + redirect_to root_url + end + end + def check_order_cycle_expiry if current_order_cycle.andand.closed? session[:expired_order_cycle_id] = current_order_cycle.id diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index 88c0f89aec..fbb5d44784 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -10,6 +10,7 @@ class BaseController < ApplicationController # include Spree::ProductsHelper so that method is available on the controller include Spree::ProductsHelper + before_filter :check_hub_ready_for_checkout before_filter :check_order_cycle_expiry def load_active_distributors diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 7e8b04afda..024c250ede 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -1,6 +1,7 @@ class CheckoutController < Spree::CheckoutController layout 'darkswarm' + prepend_before_filter :check_hub_ready_for_checkout prepend_before_filter :check_order_cycle_expiry prepend_before_filter :require_order_cycle prepend_before_filter :require_distributor_chosen diff --git a/spec/controllers/base_controller_spec.rb b/spec/controllers/base_controller_spec.rb index e431f14e3f..4337107615 100644 --- a/spec/controllers/base_controller_spec.rb +++ b/spec/controllers/base_controller_spec.rb @@ -1,26 +1,44 @@ require 'spec_helper' describe BaseController do - let(:oc) { mock_model(OrderCycle) } - let(:order) { mock_model(Spree::Order) } + let(:oc) { mock_model(OrderCycle) } + let(:hub) { mock_model(Enterprise, ready_for_checkout?: true) } + let(:order) { mock_model(Spree::Order, distributor: hub) } controller(BaseController) do def index render text: "" end end + it "redirects to home with message if order cycle is expired" do - controller.stub(:current_order_cycle).and_return oc - controller.stub(:current_order).and_return order - order.stub(:empty!) - order.stub(:set_order_cycle!) - oc.stub(:closed?).and_return true + controller.stub(:current_order_cycle).and_return(oc) + controller.stub(:current_order).and_return(order) + oc.stub(:closed?).and_return(true) + + order.should_receive(:empty!) + order.should_receive(:set_order_cycle!).with(nil) + get :index + response.should redirect_to root_url flash[:info].should == "The order cycle you've selected has just closed. Please try again!" end + it "redirects to home with message if hub is not ready for checkout" do + hub.stub(:ready_for_checkout?) { false } + controller.stub(:current_order).and_return(order) + + order.should_receive(:empty!) + order.should_receive(:set_distribution!).with(nil, nil) + + get :index + + response.should redirect_to root_url + flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." + end + it "loads active_distributors" do - Enterprise.should_receive(:distributors_with_active_order_cycles) - controller.load_active_distributors + Enterprise.stub_chain(:distributors_with_active_order_cycles, :ready_for_checkout) { 'active distributors' } + controller.load_active_distributors.should == 'active distributors' end end From e2d88e615b80d40e0fa2796a88ef31c7a7908aca Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 31 Oct 2014 16:00:28 +1100 Subject: [PATCH 154/681] Fix broken spec --- spec/features/consumer/shopping/shopping_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index b9b2a21219..beba910ec8 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -82,7 +82,7 @@ feature "As a consumer I want to shop with a distributor", js: true do page.should have_content "Next order closing in 2 days" Spree::Order.last.order_cycle.should == oc1 page.should have_content product.name - page.should have_content product.master.display_name.capitalize + page.should have_content product.master.display_name page.should have_content product.master.display_as open_product_modal product From f5b20b7afc87d25bbf8f7b5f11c20b2d7185eb9c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 31 Oct 2014 16:02:43 +1100 Subject: [PATCH 155/681] Give distributors a payment and shipping method where required --- spec/factories.rb | 11 + spec/features/consumer/home_spec.rb | 4 +- .../consumer/shopping/checkout_auth_spec.rb | 2 +- .../consumer/shopping/checkout_spec.rb | 258 +++++++++--------- .../consumer/shopping/shopping_spec.rb | 8 +- spec/features/consumer/suppliers_spec.rb | 4 +- 6 files changed, 144 insertions(+), 143 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index 60b94b08c0..0112ea8b51 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -108,6 +108,17 @@ FactoryGirl.define do factory :distributor_enterprise, :parent => :enterprise do is_primary_producer false sells "any" + + ignore do + with_payment_and_shipping false + end + + after(:create) do |enterprise, proxy| + if proxy.with_payment_and_shipping + create(:payment_method, distributors: [enterprise]) + create(:shipping_method, distributors: [enterprise]) + end + end end factory :enterprise_relationship do diff --git a/spec/features/consumer/home_spec.rb b/spec/features/consumer/home_spec.rb index 56739187a0..d08dee5dfc 100644 --- a/spec/features/consumer/home_spec.rb +++ b/spec/features/consumer/home_spec.rb @@ -4,9 +4,7 @@ feature 'Home', js: true do include AuthenticationWorkflow include UIComponentHelper - let!(:distributor) { create(:distributor_enterprise) } - let!(:shipping_method) { create(:shipping_method, distributors: [distributor]) } - let!(:payment_method) { create(:payment_method, distributors: [distributor]) } + let!(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } let!(:invisible_distributor) { create(:distributor_enterprise, visible: false) } let(:d1) { create(:distributor_enterprise) } let(:d2) { create(:distributor_enterprise) } diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index 2e756879a9..aaf543a7fa 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -7,7 +7,7 @@ feature "As a consumer I want to check out my cart", js: true do include CheckoutWorkflow include UIComponentHelper - let(:distributor) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } let(:supplier) { create(:supplier_enterprise) } let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } let(:product) { create(:simple_product, supplier: supplier) } diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index f9c05651a2..667e7abdf8 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -27,9 +27,19 @@ feature "As a consumer I want to check out my cart", js: true do page.should have_content distributor.name end - describe "with shipping methods" do + describe "with shipping and payment methods" do let(:sm1) { create(:shipping_method, require_ship_address: true, name: "Frogs", description: "yellow", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0.00)) } let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 4.56)) } + let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") } + let!(:pm2) { create(:payment_method, distributors: [distributor]) } + let!(:pm3) do + Spree::Gateway::PayPalExpress.create!(name: "Paypal", environment: 'test', distributor_ids: [distributor.id]).tap do |pm| + pm.preferred_login = 'devnull-facilitator_api1.rohanmitchell.com' + pm.preferred_password = '1406163716' + pm.preferred_signature = 'AFcWxV21C7fd0v3bYYYRCpSSRl31AaTntNJ-AjvUJkWf4dgJIvcLsf1V' + end + end + before do distributor.shipping_methods << sm1 distributor.shipping_methods << sm2 @@ -68,32 +78,62 @@ feature "As a consumer I want to check out my cart", js: true do end end - describe "with payment methods" do - let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") } - let!(:pm2) { create(:payment_method, distributors: [distributor]) } - let!(:pm3) do - Spree::Gateway::PayPalExpress.create!(name: "Paypal", environment: 'test', distributor_ids: [distributor.id]).tap do |pm| - pm.preferred_login = 'devnull-facilitator_api1.rohanmitchell.com' - pm.preferred_password = '1406163716' - pm.preferred_signature = 'AFcWxV21C7fd0v3bYYYRCpSSRl31AaTntNJ-AjvUJkWf4dgJIvcLsf1V' - end + context "on the checkout page with payments open" do + before do + visit checkout_path + checkout_as_guest + toggle_payment end - context "on the checkout page with payments open" do - before do - visit checkout_path - checkout_as_guest + it "shows all available payment methods" do + page.should have_content pm1.name + page.should have_content pm2.name + page.should have_content pm3.name + end + + describe "purchasing" do + it "takes us to the order confirmation page when we submit a complete form" do + toggle_details + within "#details" do + fill_in "First Name", with: "Will" + fill_in "Last Name", with: "Marshall" + fill_in "Email", with: "test@test.com" + fill_in "Phone", with: "0468363090" + end + toggle_billing + within "#billing" do + fill_in "Address", with: "123 Your Face" + select "Australia", from: "Country" + select "Victoria", from: "State" + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" + end + toggle_shipping + within "#shipping" do + choose sm2.name + fill_in 'Any notes or custom delivery instructions?', with: "SpEcIaL NoTeS" + end toggle_payment + within "#payment" do + choose pm1.name + end + + place_order + page.should have_content "Your order has been processed successfully" + ActionMailer::Base.deliveries.length.should == 2 + email = ActionMailer::Base.deliveries.last + site_name = Spree::Config[:site_name] + email.subject.should include "#{site_name} Order Confirmation" + o = Spree::Order.complete.first + expect(o.special_instructions).to eq "SpEcIaL NoTeS" end - it "shows all available payment methods" do - page.should have_content pm1.name - page.should have_content pm2.name - page.should have_content pm3.name - end - - describe "purchasing" do - it "takes us to the order confirmation page when we submit a complete form" do + context "with basic details filled" do + before do + toggle_shipping + choose sm1.name + toggle_payment + choose pm1.name toggle_details within "#details" do fill_in "First Name", with: "Will" @@ -103,135 +143,93 @@ feature "As a consumer I want to check out my cart", js: true do end toggle_billing within "#billing" do + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" fill_in "Address", with: "123 Your Face" select "Australia", from: "Country" select "Victoria", from: "State" - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" end toggle_shipping - within "#shipping" do - choose sm2.name - fill_in 'Any notes or custom delivery instructions?', with: "SpEcIaL NoTeS" - end - toggle_payment - within "#payment" do - choose pm1.name - end - - place_order - page.should have_content "Your order has been processed successfully" - ActionMailer::Base.deliveries.length.should == 2 - email = ActionMailer::Base.deliveries.last - site_name = Spree::Config[:site_name] - email.subject.should include "#{site_name} Order Confirmation" - o = Spree::Order.complete.first - expect(o.special_instructions).to eq "SpEcIaL NoTeS" + check "Shipping address same as billing address?" end - context "with basic details filled" do - before do - toggle_shipping - choose sm1.name - toggle_payment - choose pm1.name - toggle_details - within "#details" do - fill_in "First Name", with: "Will" - fill_in "Last Name", with: "Marshall" - fill_in "Email", with: "test@test.com" - fill_in "Phone", with: "0468363090" - end - toggle_billing - within "#billing" do - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" - fill_in "Address", with: "123 Your Face" - select "Australia", from: "Country" - select "Victoria", from: "State" - end - toggle_shipping - check "Shipping address same as billing address?" - end + it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do + place_order + page.should have_content "Your order has been processed successfully" + end - it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do + context "when we are charged a shipping fee" do + before { choose sm2.name } + + it "creates a payment for the full amount inclusive of shipping" do place_order page.should have_content "Your order has been processed successfully" + + # There are two orders - our order and our new cart + o = Spree::Order.complete.first + o.adjustments.shipping.first.amount.should == 4.56 + o.payments.first.amount.should == 10 + 1.23 + 4.56 # items + fees + shipping + end + end + + context "with a credit card payment method" do + let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::Gateway::Bogus") } + + it "takes us to the order confirmation page when submitted with a valid credit card" do + toggle_payment + fill_in 'Card Number', with: "4111111111111111" + select 'February', from: 'secrets.card_month' + select (Date.today.year+1).to_s, from: 'secrets.card_year' + fill_in 'Security Code', with: '123' + + place_order + page.should have_content "Your order has been processed successfully" + + # Order should have a payment with the correct amount + o = Spree::Order.complete.first + o.payments.first.amount.should == 11.23 end - context "when we are charged a shipping fee" do - before { choose sm2.name } + it "shows the payment processing failed message when submitted with an invalid credit card" do + toggle_payment + fill_in 'Card Number', with: "9999999988887777" + select 'February', from: 'secrets.card_month' + select (Date.today.year+1).to_s, from: 'secrets.card_year' + fill_in 'Security Code', with: '123' - it "creates a payment for the full amount inclusive of shipping" do - place_order - page.should have_content "Your order has been processed successfully" + place_order + page.should have_content "Payment could not be processed, please check the details you entered" - # There are two orders - our order and our new cart - o = Spree::Order.complete.first - o.adjustments.shipping.first.amount.should == 4.56 - o.payments.first.amount.should == 10 + 1.23 + 4.56 # items + fees + shipping - end - end - - context "with a credit card payment method" do - let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::Gateway::Bogus") } - - it "takes us to the order confirmation page when submitted with a valid credit card" do - toggle_payment - fill_in 'Card Number', with: "4111111111111111" - select 'February', from: 'secrets.card_month' - select (Date.today.year+1).to_s, from: 'secrets.card_year' - fill_in 'Security Code', with: '123' - - place_order - page.should have_content "Your order has been processed successfully" - - # Order should have a payment with the correct amount - o = Spree::Order.complete.first - o.payments.first.amount.should == 11.23 - end - - it "shows the payment processing failed message when submitted with an invalid credit card" do - toggle_payment - fill_in 'Card Number', with: "9999999988887777" - select 'February', from: 'secrets.card_month' - select (Date.today.year+1).to_s, from: 'secrets.card_year' - fill_in 'Security Code', with: '123' - - place_order - page.should have_content "Payment could not be processed, please check the details you entered" - - # Does not show duplicate shipping fee - visit checkout_path - page.all("th", text: "Shipping").count.should == 1 - end + # Does not show duplicate shipping fee + visit checkout_path + page.all("th", text: "Shipping").count.should == 1 end end end end + end - context "when the customer has a pre-set shipping and billing address" do - before do - # Load up the customer's order and give them a shipping and billing address - # This is equivalent to when the customer has ordered before and their addresses - # are pre-populated. - o = Spree::Order.last - o.ship_address = build(:address) - o.bill_address = build(:address) - o.save! - end + context "when the customer has a pre-set shipping and billing address" do + before do + # Load up the customer's order and give them a shipping and billing address + # This is equivalent to when the customer has ordered before and their addresses + # are pre-populated. + o = Spree::Order.last + o.ship_address = build(:address) + o.bill_address = build(:address) + o.save! + end - it "checks out successfully" do - visit checkout_path - checkout_as_guest - choose sm2.name - toggle_payment - choose pm1.name + it "checks out successfully" do + visit checkout_path + checkout_as_guest + choose sm2.name + toggle_payment + choose pm1.name - place_order - page.should have_content "Your order has been processed successfully" - ActionMailer::Base.deliveries.length.should == 2 - end + place_order + page.should have_content "Your order has been processed successfully" + ActionMailer::Base.deliveries.length.should == 2 end end end diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index beba910ec8..7a67942648 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -8,10 +8,8 @@ feature "As a consumer I want to shop with a distributor", js: true do describe "Viewing a distributor" do - let(:distributor) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } let(:supplier) { create(:supplier_enterprise) } - let(:shipping_method) { create(:shipping_method, distributors: [distributor]) } - let(:payment_method) { create(:payment_method, distributors: [distributor]) } let(:oc1) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), orders_close_at: 2.days.from_now) } let(:oc2) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), orders_close_at: 3.days.from_now) } let(:product) { create(:simple_product, supplier: supplier) } @@ -66,10 +64,6 @@ feature "As a consumer I want to shop with a distributor", js: true do end it "shows products after selecting an order cycle" do - # Hubs cannot be selected without a valid shipping and payment method - shipping_method - payment_method - product.master.update_attribute(:display_name, "kitten") product.master.update_attribute(:display_as, "rabbit") exchange1.variants << product.master ## add product to exchange diff --git a/spec/features/consumer/suppliers_spec.rb b/spec/features/consumer/suppliers_spec.rb index 2252b8e8c5..d110e89ca4 100644 --- a/spec/features/consumer/suppliers_spec.rb +++ b/spec/features/consumer/suppliers_spec.rb @@ -15,8 +15,8 @@ feature %q{ scenario "entering the site via a supplier's page" do # Given a supplier with some distributed products s = create(:supplier_enterprise) - d = create(:distributor_enterprise) - p = create(:simple_product, supplier: s) #, distributors: [d]) + d = create(:distributor_enterprise, with_payment_and_shipping: true) + p = create(:simple_product, supplier: s) oc = create(:simple_order_cycle, suppliers: [s], distributors: [d], variants: [p.master]) # When I visit a supplier page From 0d05c7163d5d10360634b7cacc4892d3f3a45e4d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 31 Oct 2014 16:26:13 +1100 Subject: [PATCH 156/681] Use factory shortcut for creating shipping and payment methods --- spec/features/admin/order_cycles_spec.rb | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index e5d8769982..3d310df1ba 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -65,10 +65,7 @@ feature %q{ product = create(:product, supplier: supplier) v1 = create(:variant, product: product) v2 = create(:variant, product: product) - distributor = create(:distributor_enterprise, name: 'My distributor') - create(:shipping_method, distributors: [distributor]) - create(:payment_method, distributors: [distributor]) - + distributor = create(:distributor_enterprise, name: 'My distributor', with_payment_and_shipping: true) # And some enterprise fees supplier_fee = create(:enterprise_fee, enterprise: supplier, name: 'Supplier fee') @@ -250,9 +247,7 @@ feature %q{ # And a coordinating, supplying and distributing enterprise with some products with variants coordinator = create(:distributor_enterprise, name: 'My coordinator') supplier = create(:supplier_enterprise, name: 'My supplier') - distributor = create(:distributor_enterprise, name: 'My distributor') - create(:shipping_method, distributors: [distributor]) - create(:payment_method, distributors: [distributor]) + distributor = create(:distributor_enterprise, name: 'My distributor', with_payment_and_shipping: true) product = create(:product, supplier: supplier) v1 = create(:variant, product: product) v2 = create(:variant, product: product) @@ -457,9 +452,7 @@ feature %q{ end context "when they do" do - let(:hub) { create(:distributor_enterprise) } - let!(:shipping_method) { create(:shipping_method, distributors: [hub]) } - let!(:payment_method) { create(:payment_method, distributors: [hub]) } + let(:hub) { create(:distributor_enterprise, with_payment_and_shipping: true) } let!(:oc) { create(:simple_order_cycle, distributors: [hub]) } it "does not display the warning on the dashboard" do From 543368169f5aa09b6646aa5440b44f2ce6c96629 Mon Sep 17 00:00:00 2001 From: Paul Mackay Date: Fri, 31 Oct 2014 06:59:43 +0000 Subject: [PATCH 157/681] #254: Localisation of home page and welcome email. --- app/views/home/_beta.en-GB.html.haml | 13 ++++++++++++ app/views/shared/_footer.html.haml | 6 +++--- .../components/_enterprise_search.html.haml | 2 +- .../signup_confirmation.en-GB.text.erb | 14 +++++++++++++ config/locales/en-GB.yml | 21 +++++++++++++++++++ config/locales/en.yml | 1 + 6 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 app/views/home/_beta.en-GB.html.haml create mode 100644 app/views/spree/user_mailer/signup_confirmation.en-GB.text.erb create mode 100644 config/locales/en-GB.yml diff --git a/app/views/home/_beta.en-GB.html.haml b/app/views/home/_beta.en-GB.html.haml new file mode 100644 index 0000000000..2badc7c826 --- /dev/null +++ b/app/views/home/_beta.en-GB.html.haml @@ -0,0 +1,13 @@ +#beta.pane + + .row + .small-12.columns.text-center + %h2 S'cuse us + %h5 while we get (more) awesome + %p Open Food Network UK is a new service that’s being piloted right now! + %p Want to help? Or find out when OFN is coming to you? + %strong We’d love to hear from you: + %p + %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food buyers + | + %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food producers & farmers diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index f8fa116f5a..c5f44988d6 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -44,9 +44,9 @@ %p %small %a{href:"/Terms-of-service.pdf", target: "_blank" } Site terms & conditions - | - %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank" } Open Source & developer info on Github - + | + %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank" } Open Source & developer info on GitHub + // To be added when Guy's pretty landing page is up: //| //%a{href:'' } Developers diff --git a/app/views/shared/components/_enterprise_search.html.haml b/app/views/shared/components/_enterprise_search.html.haml index 0ccb23220c..62dcec3ea6 100644 --- a/app/views/shared/components/_enterprise_search.html.haml +++ b/app/views/shared/components/_enterprise_search.html.haml @@ -2,6 +2,6 @@ .small-12.columns %input{type: :text, "ng-model" => "query", - placeholder: "Search by name or suburb...", + placeholder: t('search_by_name'), "ng-debounce" => "150", "ofn-disable-enter" => true} diff --git a/app/views/spree/user_mailer/signup_confirmation.en-GB.text.erb b/app/views/spree/user_mailer/signup_confirmation.en-GB.text.erb new file mode 100644 index 0000000000..2af9144ddf --- /dev/null +++ b/app/views/spree/user_mailer/signup_confirmation.en-GB.text.erb @@ -0,0 +1,14 @@ +Hello, + +Welcome to Open Food Network UK! Your login email is <%= @user.email %> + +You can go online and start shopping through food hubs and local producers you like at http://openfoodnetwork.org.uk + +We welcome all your questions and feedback; you can use the Send Feedback button on the site or email us at hello@openfoodnetwork.org. + +Thanks for getting on board and we look forward to introducing you to many more great farmers, food hubs and food! + +Cheers, + +The Open Food Network UK team + diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml new file mode 100644 index 0000000000..71186a0a52 --- /dev/null +++ b/config/locales/en-GB.yml @@ -0,0 +1,21 @@ +# Localization file for British English. Add more files in this directory for other locales. +# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. + +en-GB: + activerecord: + errors: + models: + spree/user: + attributes: + password: + confirmation: you have successfully registered + too_short: pick a longer name + devise: + failure: + invalid: | + Invalid email or password. + Were you a guest last time? Perhaps you need to create an account or reset your password. + home: "OFN" + + welcome_to: 'Welcome to ' + search_by_name: Search by name... diff --git a/config/locales/en.yml b/config/locales/en.yml index 445f629e22..378e411406 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -8,3 +8,4 @@ en: Invalid email or password. Were you a guest last time? Perhaps you need to create an account or reset your password. home: "OFN" + search_by_name: Search by name or suburb... From 0cf4e01dde93bb1865dc9112a0667b1127713106 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Sat, 1 Nov 2014 10:39:29 +1100 Subject: [PATCH 158/681] Fix specs: Expect proper query chain --- spec/controllers/home_controller_spec.rb | 3 ++- spec/controllers/map_controller_spec.rb | 8 +++++++- spec/controllers/producers_controller_spec.rb | 5 +++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index 5cd2b0cda8..16367eb812 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -6,7 +6,8 @@ describe HomeController do let!(:invisible_distributor) { create(:distributor_enterprise, visible: false) } before do - Enterprise.stub(:distributors_with_active_order_cycles).and_return [distributor] + Enterprise.stub_chain(:distributors_with_active_order_cycles, :ready_for_checkout). + and_return [distributor] end it "sets active distributors" do diff --git a/spec/controllers/map_controller_spec.rb b/spec/controllers/map_controller_spec.rb index f3d56853f9..c4e98def8a 100644 --- a/spec/controllers/map_controller_spec.rb +++ b/spec/controllers/map_controller_spec.rb @@ -2,7 +2,13 @@ require 'spec_helper' describe MapController do it "loads active distributors" do - Enterprise.should_receive(:distributors_with_active_order_cycles) + active_distributors = double(:distributors) + + Enterprise.stub_chain(:distributors_with_active_order_cycles, :ready_for_checkout). + and_return(active_distributors) + get :index + + assigns(:active_distributors).should == active_distributors end end diff --git a/spec/controllers/producers_controller_spec.rb b/spec/controllers/producers_controller_spec.rb index 812b08892f..2b7377a471 100644 --- a/spec/controllers/producers_controller_spec.rb +++ b/spec/controllers/producers_controller_spec.rb @@ -4,8 +4,9 @@ describe ProducersController do let!(:distributor) { create(:distributor_enterprise) } before do - Enterprise.stub(:distributors_with_active_order_cycles).and_return [distributor] - Enterprise.stub(:all).and_return [distributor] + Enterprise.stub_chain(:distributors_with_active_order_cycles, :ready_for_checkout). + and_return([distributor]) + Enterprise.stub(:all).and_return([distributor]) end it "sets active distributors" do From 15b05de7f2978d02f918cd88bf395762667b6783 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Sat, 1 Nov 2014 10:42:38 +1100 Subject: [PATCH 159/681] Fix specs: Distributors require shipping and payment method --- app/controllers/application_controller.rb | 7 ++++++- spec/controllers/base_controller_spec.rb | 1 + .../enterprises_controller_spec.rb | 20 ++----------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3b76ee3168..e96a0f316e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -33,7 +33,12 @@ class ApplicationController < ActionController::Base end def check_hub_ready_for_checkout - if current_distributor && !current_distributor.ready_for_checkout? + # This condition is more rigourous than required by development to avoid coupling this + # condition to every controller spec + if current_distributor && current_order && + current_distributor.respond_to?(:ready_for_checkout?) && + !current_distributor.ready_for_checkout? + current_order.empty! current_order.set_distribution! nil, nil flash[:info] = "The hub you have selected is temporarily closed for orders. Please try again later." diff --git a/spec/controllers/base_controller_spec.rb b/spec/controllers/base_controller_spec.rb index 4337107615..b340112ce2 100644 --- a/spec/controllers/base_controller_spec.rb +++ b/spec/controllers/base_controller_spec.rb @@ -20,6 +20,7 @@ describe BaseController do get :index + session[:expired_order_cycle_id].should == oc.id response.should redirect_to root_url flash[:info].should == "The order cycle you've selected has just closed. Please try again!" end diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index c0864b6a0c..fa8ca888e8 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -54,8 +54,8 @@ describe EnterprisesController do describe "shopping for a distributor" do before(:each) do - @current_distributor = create(:distributor_enterprise) - @distributor = create(:distributor_enterprise) + @current_distributor = create(:distributor_enterprise, with_payment_and_shipping: true) + @distributor = create(:distributor_enterprise, with_payment_and_shipping: true) @order_cycle1 = create(:simple_order_cycle, distributors: [@distributor]) @order_cycle2 = create(:simple_order_cycle, distributors: [@distributor]) controller.current_order(true).distributor = @current_distributor @@ -109,20 +109,4 @@ describe EnterprisesController do response.should redirect_to spree.root_path end end - - describe "BaseController: handling order cycles closing mid-order" do - it "clears the order and displays an expiry message" do - oc = double(:order_cycle, id: 123, closed?: true) - controller.stub(:current_order_cycle) { oc } - - order = double(:order) - order.should_receive(:empty!) - order.should_receive(:set_order_cycle!).with(nil) - controller.stub(:current_order) { order } - - spree_get :index - session[:expired_order_cycle_id].should == 123 - response.should redirect_to root_url - end - end end From 8c2adb2a0512632df4aa4f5305c4fe85b0b01a29 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Sat, 1 Nov 2014 12:11:50 +1100 Subject: [PATCH 160/681] Fix specs: feature specs requiring payment and shipping methods, race condition, double flash message issue --- spec/features/admin/orders_spec.rb | 2 +- spec/features/consumer/shopping/shopping_spec.rb | 7 +++---- spec/requests/shop_spec.rb | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 908ce4042e..bb38e5d8ba 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -95,7 +95,7 @@ feature %q{ # click the 'capture' link for the order page.find("[data-action=capture][href*=#{@order.number}]").click - flash_message.should == "Payment Updated" + page.should have_content "Payment Updated" # check the order was captured @order.reload diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 7a67942648..e70377cffc 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -171,11 +171,10 @@ feature "As a consumer I want to shop with a distributor", js: true do fill_in "variants[#{variant.id}]", with: 6 fill_in "variant_attributes[#{variant.id}][max_quantity]", with: 7 page.should have_in_cart product.name + + wait_until { !cart_dirty } + li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last - while li == nil - sleep 0.1 - li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last - end li.max_quantity.should == 7 li.quantity.should == 6 end diff --git a/spec/requests/shop_spec.rb b/spec/requests/shop_spec.rb index 7f4f4395e6..c13cfc675f 100644 --- a/spec/requests/shop_spec.rb +++ b/spec/requests/shop_spec.rb @@ -4,7 +4,7 @@ describe "Shop API" do include ShopWorkflow describe "filtering products" do - let(:distributor) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } 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) } From 6f6ae309c6ea0b7bbc8785d989a7b03f0b7242b0 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Sun, 2 Nov 2014 11:26:39 +0000 Subject: [PATCH 161/681] 266 UK: Adding first UK report - Payment Methods Report - to find balances per ordercycle for multiple payment method options. Working, but not complete to spec yet --- .../admin/reports_controller_decorator.rb | 23 ++++++- app/helpers/spree/reports_helper.rb | 19 ++++++ app/models/spree/ability_decorator.rb | 2 +- ...der_cycle_management_description.html.haml | 4 ++ .../reports/order_cycle_management.html.haml | 38 +++++++++++ config/routes.rb | 1 + .../order_cycle_management_report.rb | 65 +++++++++++++++++++ spec/features/admin/reports_spec.rb | 16 +++++ 8 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 app/views/spree/admin/reports/_order_cycle_management_description.html.haml create mode 100644 app/views/spree/admin/reports/order_cycle_management.html.haml create mode 100644 lib/open_food_network/order_cycle_management_report.rb diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 22dd41549f..7ffb32d097 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -4,6 +4,7 @@ require 'open_food_network/products_and_inventory_report' require 'open_food_network/group_buy_report' require 'open_food_network/order_grouper' require 'open_food_network/customers_report' +require 'open_food_network/order_cycle_management_report' Spree::Admin::ReportsController.class_eval do @@ -21,11 +22,14 @@ Spree::Admin::ReportsController.class_eval do customers: [ ["Mailing List", :mailing_list], ["Addresses", :addresses] + ], + order_cycle_management: [ + ["Payment Methods Report", :payment_methods_report] ] } # Fetches user's distributors, suppliers and order_cycles - before_filter :load_data, only: [:customers, :products_and_inventory] + before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management] # Render a partial for orders and fulfillment description respond_override :index => { :html => { :success => lambda { @@ -35,6 +39,8 @@ Spree::Admin::ReportsController.class_eval do render_to_string(partial: 'products_and_inventory_description', layout: false, locals: {report_types: REPORT_TYPES[:products_and_inventory]}).html_safe @reports[:customers][:description] = render_to_string(partial: 'customers_description', layout: false, locals: {report_types: REPORT_TYPES[:customers]}).html_safe + @reports[:order_cycle_management][:description] = + render_to_string(partial: 'order_cycle_management_description', layout: false, locals: {report_types: REPORT_TYPES[:order_cycle_management]}).html_safe } } } @@ -53,6 +59,18 @@ Spree::Admin::ReportsController.class_eval do render_report(@report.header, @report.table, params[:csv], "customers.csv") end + def order_cycle_management + @report_types = REPORT_TYPES[:order_cycle_management] + @report_type = params[:report_type] + @report = OpenFoodNetwork::OrderCycleManagementReport.new spree_current_user, params + + @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) + + @orders = @search.result + + render_report(@report.header, @report.table, params[:csv], "customers.csv") + end + def orders_and_distributors params[:q] = {} unless params[:q] @@ -613,7 +631,8 @@ Spree::Admin::ReportsController.class_eval do :orders_and_fulfillment => {:name => "Orders & Fulfillment Reports", :description => ''}, :customers => {:name => "Customers", :description => 'Customer details'}, :products_and_inventory => {:name => "Products & Inventory", :description => ''}, - :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" } + :order_cycle_management => {:name => "UK Order Cycle Management", :description => ''} + } # Return only reports the user is authorized to view. reports.select { |action| can? action, :report } diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index 573b790051..0378e7db46 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -7,5 +7,24 @@ module Spree [ "#{oc.name}   (#{orders_open_at} - #{orders_close_at})".html_safe, oc.id ] end end + + #lin-d-hop + #Find the payment methods options for reporting. + #I don't like that this is done in two loops, but redundant list entries + # were created otherwise... + def report_payment_method_options(orders) + payment_method_list = {} + orders.map do |o| + payment_method_name = o.payments.first.payment_method.andand.name + payment_method_id = o.payments.first.payment_method.andand.id + payment_method_list[payment_method_name] = payment_method_id + end + payment_method_list.each do |key, value| + [ "#{value}".html_safe, key] + end + end + + + end end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 23994a4580..e0f4663831 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -122,7 +122,7 @@ class AbilityDecorator end # Reports page - can [:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], :report + can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report end diff --git a/app/views/spree/admin/reports/_order_cycle_management_description.html.haml b/app/views/spree/admin/reports/_order_cycle_management_description.html.haml new file mode 100644 index 0000000000..d4d20ae66e --- /dev/null +++ b/app/views/spree/admin/reports/_order_cycle_management_description.html.haml @@ -0,0 +1,4 @@ +%ul{style: "margin-left: 12pt"} + - report_types.each do |report_type| + %li + = link_to report_type[0], "#{order_cycle_management_admin_reports_url}?report_type=#{report_type[1]}" diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml new file mode 100644 index 0000000000..a88e55ddde --- /dev/null +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -0,0 +1,38 @@ += form_tag spree.order_cycle_management_admin_reports_url do |f| + %br + = label_tag nil, "Order Cycle: " + = select_tag(:order_cycle_id, + options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), + include_blank: true) + %br + %br + = label_tag nil, "Payment Methods (hold Ctrl to select multiple payment methods)" + %br + + = select_tag(:payment_method_id, + options_for_select(report_payment_method_options(@orders), params[:payment_method_id]), + multiple: true, include_blank: true) + %br + %br + = check_box_tag :csv + = label_tag :csv, "Download as csv" + %br + %br + = button t(:search) + +%br +%br +%table#listing_order_payment_methods.index + %thead + %tr{'data-hook' => "orders_header"} + - @report.header.each do |heading| + %th=heading + %tbody + - @report.table.each do |row| + %tr + - row.each do |column| + %td= column + - if @report.table.empty? + %tr + %td{:colspan => "2"}= t(:none) + diff --git a/config/routes.rb b/config/routes.rb index 62f55065fc..608f9a0f3b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -112,6 +112,7 @@ end Spree::Core::Engine.routes.prepend do match '/admin/reports/orders_and_distributors' => 'admin/reports#orders_and_distributors', :as => "orders_and_distributors_admin_reports", :via => [:get, :post] + match '/admin/reports/order_cycle_management' => 'admin/reports#order_cycle_management', :as => "order_cycle_management_admin_reports", :via => [:get, :post] match '/admin/reports/group_buys' => 'admin/reports#group_buys', :as => "group_buys_admin_reports", :via => [:get, :post] match '/admin/reports/bulk_coop' => 'admin/reports#bulk_coop', :as => "bulk_coop_admin_reports", :via => [:get, :post] match '/admin/reports/payments' => 'admin/reports#payments', :as => "payments_admin_reports", :via => [:get, :post] diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb new file mode 100644 index 0000000000..70c5de8f29 --- /dev/null +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -0,0 +1,65 @@ +module OpenFoodNetwork + class OrderCycleManagementReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + end + + def header + ["First Name", "Last Name", "Email", "Phone", "Hub", "Payment Method", "Amount ", "Amount Paid"] + + end + + def table + orders.map do |order| + ba = order.billing_address + da = order.distributor.andand.address + [ba.firstname, + ba.lastname, + order.email, + ba.phone, + order.distributor.andand.name, + order.payments.first.andand.payment_method.andand.name, + order.payments.first.amount + ] + end + end + + def orders + filter Spree::Order + end + + def filter(orders) + filter_for_user filter_active filter_to_order_cycle filter_to_payment_method orders + end + + def filter_for_user (orders) + orders.managed_by(@user).distributed_by_user(@user) + end + + def filter_active (orders) + orders.complete.where("spree_orders.state != ?", :cancelled) + end + + def filter_to_payment_method (orders) + if params.has_key? (:payment_method_id) + orders.joins(:payments) + .where(:spree_payments => {:payment_method_id => params[:payment_method_id]} ) + else + orders + end + end + + def filter_to_order_cycle(orders) + if params[:order_cycle_id].to_i > 0 + orders.where(order_cycle_id: params[:order_cycle_id]) + else + orders + end + end + + + end +end + diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index a1f1bcfec3..82cdbf59ba 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -57,6 +57,22 @@ feature %q{ end end + describe "Order cycle management report" do + before do + login_to_admin_section + click_link "Reports" + end + + scenario "order payment method report" do + click_link "Order Cycle Management" + rows = find("table#listing_order_payment_method").all("thead tr") + table = rows.map { |r| r.all("th").map { |c| c.text.strip } } + table.sort.should == [ + ["First Name", "Last Name", "Email", "Phone", "Hub", "Payment Method", "Amount", "Amount Paid"] + ].sort + end + end + scenario "orders and distributors report" do login_to_admin_section click_link 'Reports' From fdbb274667d44f2535042c29fa944af85d1b0380 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 5 Nov 2014 14:29:36 +1100 Subject: [PATCH 162/681] Allow browsing products (but not cart/checkout) for hubs that are not ready for checkout --- app/controllers/base_controller.rb | 1 - .../spree/orders_controller_decorator.rb | 5 +++-- spec/controllers/base_controller_spec.rb | 13 ------------- spec/controllers/checkout_controller_spec.rb | 14 ++++++++++++++ spec/controllers/spree/orders_controller_spec.rb | 14 ++++++++++++++ 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index fbb5d44784..88c0f89aec 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -10,7 +10,6 @@ class BaseController < ApplicationController # include Spree::ProductsHelper so that method is available on the controller include Spree::ProductsHelper - before_filter :check_hub_ready_for_checkout before_filter :check_order_cycle_expiry def load_active_distributors diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index e9245b4a32..a1b468ae2d 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -5,8 +5,9 @@ Spree::OrdersController.class_eval do before_filter :update_distribution, :only => :update before_filter :filter_order_params, :only => :update - prepend_before_filter :require_order_cycle, only: [:edit] - prepend_before_filter :require_distributor_chosen, only: [:edit] + prepend_before_filter :require_order_cycle, only: :edit + prepend_before_filter :require_distributor_chosen, only: :edit + before_filter :check_hub_ready_for_checkout, only: :edit include OrderCyclesHelper layout 'darkswarm' diff --git a/spec/controllers/base_controller_spec.rb b/spec/controllers/base_controller_spec.rb index b340112ce2..822da57ba5 100644 --- a/spec/controllers/base_controller_spec.rb +++ b/spec/controllers/base_controller_spec.rb @@ -25,19 +25,6 @@ describe BaseController do flash[:info].should == "The order cycle you've selected has just closed. Please try again!" end - it "redirects to home with message if hub is not ready for checkout" do - hub.stub(:ready_for_checkout?) { false } - controller.stub(:current_order).and_return(order) - - order.should_receive(:empty!) - order.should_receive(:set_distribution!).with(nil, nil) - - get :index - - response.should redirect_to root_url - flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." - end - it "loads active_distributors" do Enterprise.stub_chain(:distributors_with_active_order_cycles, :ready_for_checkout) { 'active distributors' } controller.load_active_distributors.should == 'active distributors' diff --git a/spec/controllers/checkout_controller_spec.rb b/spec/controllers/checkout_controller_spec.rb index 86e9689aa2..dcbd1d5177 100644 --- a/spec/controllers/checkout_controller_spec.rb +++ b/spec/controllers/checkout_controller_spec.rb @@ -20,6 +20,20 @@ describe CheckoutController do response.should redirect_to shop_path end + it "redirects home with message if hub is not ready for checkout" do + distributor.stub(:ready_for_checkout?) { false } + order.stub(distributor: distributor, order_cycle: order_cycle) + controller.stub(:current_order).and_return(order) + + order.should_receive(:empty!) + order.should_receive(:set_distribution!).with(nil, nil) + + get :edit + + response.should redirect_to root_url + flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." + end + it "redirects to the shop when no line items are present" do controller.stub(:current_distributor).and_return(distributor) controller.stub(:current_order_cycle).and_return(order_cycle) diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 354ebb8701..1d2f41f86c 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -26,6 +26,20 @@ describe Spree::OrdersController do response.should redirect_to shop_path end + it "redirects home with message if hub is not ready for checkout" do + order = subject.current_order(true) + distributor.stub(:ready_for_checkout?) { false } + order.stub(distributor: distributor, order_cycle: order_cycle) + + order.should_receive(:empty!) + order.should_receive(:set_distribution!).with(nil, nil) + + spree_get :edit + + response.should redirect_to root_url + flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." + end + it "selects distributors" do d = create(:distributor_enterprise) p = create(:product, :distributors => [d]) From 1506b10d8f7c2ceb14eccc9e8765b520aab6baf0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 5 Nov 2014 16:08:45 +1100 Subject: [PATCH 163/681] Fix brittle spec --- spec/features/admin/enterprises_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index ae93ac19ea..3383717460 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -104,7 +104,7 @@ feature %q{ page.should have_content e.name end - scenario "creating a new enterprise", js:true do + scenario "creating a new enterprise", js: true do eg1 = create(:enterprise_group, name: 'eg1') eg2 = create(:enterprise_group, name: 'eg2') payment_method = create(:payment_method) @@ -125,6 +125,10 @@ feature %q{ # Filling in details fill_in 'enterprise_name', :with => 'Eaterprises' + # This call intermittently fails to complete, leaving the select2 box open obscuring the + # fields below it (which breaks the remainder of our specs). Calling it twice seems to + # solve the problem. + select2_search admin.email, from: 'Owner' select2_search admin.email, from: 'Owner' choose 'Any' check "enterprise_payment_method_ids_#{payment_method.id}" From 93e3d87fcbcfe6235c4baeb7ed6869f77e2cb5b4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 6 Nov 2014 11:01:44 +1100 Subject: [PATCH 164/681] Errors on bulk update for enterprise index do not allow user to list all enterprises --- app/controllers/admin/enterprises_controller.rb | 2 +- spec/controllers/admin/enterprises_controller_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index b8dd9b9bd5..96e77f129d 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -42,7 +42,7 @@ module Admin end def bulk_update - @enterprise_set = EnterpriseSet.new(params[:enterprise_set]) + @enterprise_set = EnterpriseSet.new(params[:enterprise_set].merge collection: collection) if @enterprise_set.save flash[:success] = 'Enterprises updated successfully' redirect_to main_app.admin_enterprises_path diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 1e961e3710..cda73d4079 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -254,6 +254,15 @@ module Admin expect(profile_enterprise1.owner).to eq original_owner expect(profile_enterprise2.owner).to eq original_owner end + + it "cuts down the list of enterprises displayed when error received on bulk update" do + EnterpriseSet.any_instance.stub(:save) { false } + profile_enterprise1.enterprise_roles.build(user: new_owner).save + controller.stub spree_current_user: new_owner + bulk_enterprise_params = { enterprise_set: { collection_attributes: { '0' => { id: profile_enterprise1.id, visible: 'false' } } } } + spree_put :bulk_update, bulk_enterprise_params + expect(assigns(:enterprise_set).collection).to eq [profile_enterprise1] + end end context "as super admin" do From be042af7517f046d72f153fd0df6888b61d8ac88 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 6 Nov 2014 15:54:59 +1100 Subject: [PATCH 165/681] Add explicit select statement to prevent scope being read only --- app/models/enterprise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 0840a53df3..0c98bcc7bf 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -137,7 +137,7 @@ class Enterprise < ActiveRecord::Base if user.has_spree_role?('admin') scoped else - joins(:enterprise_roles).where('enterprise_roles.user_id = ?', user.id) + joins(:enterprise_roles).where('enterprise_roles.user_id = ?', user.id).select("DISTINCT enterprises.*") end } From b27a0986b701570fc877e4c46be347bf1281571d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 6 Nov 2014 16:28:24 +1100 Subject: [PATCH 166/681] Ensure collection is locked down before we start asssigning attributes to model set --- app/models/model_set.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/model_set.rb b/app/models/model_set.rb index d4760884b5..24e84008a0 100644 --- a/app/models/model_set.rb +++ b/app/models/model_set.rb @@ -8,6 +8,9 @@ class ModelSet def initialize(klass, collection, reject_if=nil, attributes={}) @klass, @collection, @reject_if = klass, collection, reject_if + # Set here first, to ensure that we apply collection_attributes to the right collection + @collection = attributes[:collection] if attributes[:collection] + attributes.each do |name, value| send("#{name}=", value) end From 51e768e7228123458d894f25efb6e3265edcbc59 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 6 Nov 2014 16:29:09 +1100 Subject: [PATCH 167/681] Refactor EnterpriseSet to accept a collection as an argument --- app/controllers/admin/enterprises_controller.rb | 4 ++-- app/models/enterprise_set.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 96e77f129d..c216b1badf 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -42,7 +42,7 @@ module Admin end def bulk_update - @enterprise_set = EnterpriseSet.new(params[:enterprise_set].merge collection: collection) + @enterprise_set = EnterpriseSet.new(collection, params[:enterprise_set]) if @enterprise_set.save flash[:success] = 'Enterprises updated successfully' redirect_to main_app.admin_enterprises_path @@ -67,7 +67,7 @@ module Admin private def load_enterprise_set - @enterprise_set = EnterpriseSet.new :collection => collection + @enterprise_set = EnterpriseSet.new collection end def load_countries diff --git a/app/models/enterprise_set.rb b/app/models/enterprise_set.rb index 81591b6dad..0eef0ea3c8 100644 --- a/app/models/enterprise_set.rb +++ b/app/models/enterprise_set.rb @@ -1,5 +1,5 @@ class EnterpriseSet < ModelSet - def initialize(attributes={}) - super(Enterprise, Enterprise.all, nil, attributes) + def initialize(collection, attributes={}) + super(Enterprise, collection, nil, attributes) end end From e8e5f7033c65c622162599fc506ff8bfb7828d83 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 12:44:57 +1100 Subject: [PATCH 168/681] Improve all the syntax --- .../controllers/checkout/checkout_controller.js.coffee | 2 +- app/views/checkout/_form.html.haml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee index 9c953c4f4a..11f65fe358 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee @@ -15,7 +15,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, storage, Checkout, CurrentUser, Cu $scope.order = Checkout.order # Ordering is important $scope.secrets = Checkout.secrets - $scope.enabled = if CurrentUser then true else false + $scope.enabled = !!CurrentUser $scope.purchase = (event)-> event.preventDefault() diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index 91153bdb4b..af81993fb0 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -7,10 +7,10 @@ = inject_available_shipping_methods = inject_available_payment_methods - = render partial: "checkout/details", locals: {f: f} - = render partial: "checkout/billing", locals: {f: f} - = render partial: "checkout/shipping", locals: {f: f} - = render partial: "checkout/payment", locals: {f: f} + = render "checkout/details", f: f + = render "checkout/billing", f: f + = render "checkout/shipping", f: f + = render "checkout/payment", f: f %p %button.button.primary{type: :submit, "ng-disabled" => "checkout.$invalid"} From 75b250d3fe70dce76234d6e1846756b6dc12df6a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 12:53:02 +1100 Subject: [PATCH 169/681] When submitting checkout, if field is invalid, show errors --- .../checkout/checkout_controller.js.coffee | 4 ++- .../darkswarm/mixins/fieldset_mixin.js.coffee | 2 +- app/views/checkout/_billing.html.haml | 2 +- app/views/checkout/_details.html.haml | 2 +- app/views/checkout/_form.html.haml | 3 +-- app/views/checkout/_payment.html.haml | 2 +- app/views/checkout/_shipping.html.haml | 2 +- .../checkout_controller_spec.js.coffee | 25 +++++++++++++++---- 8 files changed, 29 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee index 11f65fe358..2ab0da6ff6 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee @@ -1,5 +1,6 @@ Darkswarm.controller "CheckoutCtrl", ($scope, storage, Checkout, CurrentUser, CurrentHub) -> $scope.Checkout = Checkout + $scope.submitted = false # Bind to local storage $scope.fieldsToBind = ["bill_address", "email", "payment_method_id", "shipping_method_id", "ship_address"] @@ -19,4 +20,5 @@ Darkswarm.controller "CheckoutCtrl", ($scope, storage, Checkout, CurrentUser, Cu $scope.purchase = (event)-> event.preventDefault() - $scope.Checkout.submit() + $scope.submitted = true + $scope.Checkout.submit() if $scope.checkout.$valid diff --git a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee index b9f59e93a8..ee4058221c 100644 --- a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee +++ b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee @@ -20,7 +20,7 @@ window.FieldsetMixin = ($scope)-> not ($scope.dirty(path) and $scope.invalid(path)) $scope.dirty = (name)-> - $scope.field(name).$dirty + $scope.field(name).$dirty || $scope.submitted $scope.invalid = (name)-> $scope.field(name).$invalid diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index c052eb1b05..f9b42827db 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -1,7 +1,7 @@ %fieldset#billing %ng-form{"ng-controller" => "BillingCtrl", name: "billing"} - %h5{"ng-class" => "{valid: billing.$valid, dirty: billing.$dirty}"} + %h5{"ng-class" => "{valid: billing.$valid, dirty: billing.$dirty || submitted}"} %span.right %label.label.round.alert.right %i.ofn-i_009-close diff --git a/app/views/checkout/_details.html.haml b/app/views/checkout/_details.html.haml index 6eb149804f..20407cbfd5 100644 --- a/app/views/checkout/_details.html.haml +++ b/app/views/checkout/_details.html.haml @@ -1,7 +1,7 @@ %fieldset#details %ng-form{"ng-controller" => "DetailsCtrl", name: "details"} - %h5{"ng-class" => "{valid: details.$valid, dirty: details.$dirty}"} + %h5{"ng-class" => "{valid: details.$valid, dirty: details.$dirty || submitted}"} %span.right %label.label.round.alert.right %i.ofn-i_009-close diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index af81993fb0..4d7749760c 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -12,7 +12,6 @@ = render "checkout/shipping", f: f = render "checkout/payment", f: f %p - %button.button.primary{type: :submit, - "ng-disabled" => "checkout.$invalid"} + %button.button.primary{type: :submit} Place order now / {{ checkout.$valid }} diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index 2ebc956b59..e58e014f50 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -1,7 +1,7 @@ %fieldset#payment %ng-form{"ng-controller" => "PaymentCtrl", name: "payment"} - %h5{"ng-class" => "{valid: payment.$valid, dirty: payment.$dirty}"} + %h5{"ng-class" => "{valid: payment.$valid, dirty: payment.$dirty || submitted}"} %span.right %label.label.round.alert.right %i.ofn-i_009-close diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index b535908c53..9183f588f8 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -1,7 +1,7 @@ %fieldset#shipping %ng-form{"ng-controller" => "ShippingCtrl", name: "shipping"} - %h5{"ng-class" => "{valid: shipping.$valid, dirty: shipping.$dirty}"} + %h5{"ng-class" => "{valid: shipping.$valid, dirty: shipping.$dirty || submitted}"} %span.right %label.label.round.alert.right %i.ofn-i_009-close diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee index fdbc4742a3..9cd2c5a06c 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee @@ -33,13 +33,28 @@ describe "CheckoutCtrl", -> spyOn(storage, "bind").andCallThrough() scope = $rootScope.$new() ctrl = $controller 'CheckoutCtrl', {$scope: scope, Checkout: Checkout, CurrentUser: {}} - - it "delegates to the service on submit", -> + + describe "submitting", -> event = preventDefault: -> - spyOn(Checkout, "submit") - scope.purchase(event) - expect(Checkout.submit).toHaveBeenCalled() + + beforeEach -> + spyOn(Checkout, "submit") + scope.submitted = false + + it "delegates to the service when valid", -> + scope.checkout = + $valid: true + scope.purchase(event) + expect(Checkout.submit).toHaveBeenCalled() + expect(scope.submitted).toBe(true) + + it "does nothing when invalid", -> + scope.checkout = + $valid: false + scope.purchase(event) + expect(Checkout.submit).not.toHaveBeenCalled() + expect(scope.submitted).toBe(true) it "is enabled", -> expect(scope.enabled).toEqual true From cbb968d87f29f757075414c9123d78e58b38c3a6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 14:11:31 +1100 Subject: [PATCH 170/681] Pass checkout form to controller method --- .../controllers/checkout/checkout_controller.js.coffee | 4 ++-- app/views/checkout/_form.html.haml | 2 +- app/views/checkout/_summary.html.haml | 4 ++-- .../checkout/checkout_controller_spec.js.coffee | 8 ++------ 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee index 2ab0da6ff6..ed99c6bd41 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee @@ -18,7 +18,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, storage, Checkout, CurrentUser, Cu $scope.enabled = !!CurrentUser - $scope.purchase = (event)-> + $scope.purchase = (event, form) -> event.preventDefault() $scope.submitted = true - $scope.Checkout.submit() if $scope.checkout.$valid + $scope.Checkout.submit() if form.$valid diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index 4d7749760c..1944dfffe1 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -2,7 +2,7 @@ html: {name: "checkout", id: "checkout_form", novalidate: true, - "ng-submit" => "purchase($event)"} do |f| + "ng-submit" => "purchase($event, checkout)"} do |f| = inject_available_shipping_methods = inject_available_payment_methods diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index 8609fd7a16..df454e1701 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -1,5 +1,5 @@ %orderdetails - = form_for current_order, url: "#", html: {"ng-submit" => "purchase($event)"} do |f| + = form_for current_order, url: "#", html: {"ng-submit" => "purchase($event, checkout)"} do |f| %fieldset %legend Your order %table @@ -25,7 +25,7 @@ %th= label %td= total - //= f.submit "Purchase", class: "button", "ng-disabled" => "checkout.$invalid", "ofn-focus" => "accordion['payment']" + //= f.submit "Purchase", class: "button", "ofn-focus" => "accordion['payment']" %a.button.secondary{href: cart_url} %i.ofn-i_008-caret-left Back to Cart diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee index 9cd2c5a06c..82b402d50b 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee @@ -43,16 +43,12 @@ describe "CheckoutCtrl", -> scope.submitted = false it "delegates to the service when valid", -> - scope.checkout = - $valid: true - scope.purchase(event) + scope.purchase(event, {$valid: true}) expect(Checkout.submit).toHaveBeenCalled() expect(scope.submitted).toBe(true) it "does nothing when invalid", -> - scope.checkout = - $valid: false - scope.purchase(event) + scope.purchase(event, {$valid: false}) expect(Checkout.submit).not.toHaveBeenCalled() expect(scope.submitted).toBe(true) From e50d2a6a37e0b0dafebaf8bfb968f1f8040536ce Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 14:11:42 +1100 Subject: [PATCH 171/681] Remove unused directive --- .../darkswarm/directives/submit_checkout.js.coffee | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 app/assets/javascripts/darkswarm/directives/submit_checkout.js.coffee diff --git a/app/assets/javascripts/darkswarm/directives/submit_checkout.js.coffee b/app/assets/javascripts/darkswarm/directives/submit_checkout.js.coffee deleted file mode 100644 index 207a76049a..0000000000 --- a/app/assets/javascripts/darkswarm/directives/submit_checkout.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -Darkswarm.directive "submitCheckout", () -> - restrict: "A" - link: (scope, elm, attr)-> - elm.bind 'click', (ev)-> - ev.preventDefault() - - names = ["details", "billing", "shipping", "payment"] - for name of names - if not scope[name].$valid - $scope.show name - # else - # scope.purchase(ev) - From 900a98b4bb05ef00eaed776f84e3a8ad63b4553a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 14:33:35 +1100 Subject: [PATCH 172/681] Display errors on checkout State field --- app/helpers/checkout_helper.rb | 13 ++++++++++++- app/views/checkout/_billing.html.haml | 3 +-- app/views/shared/_validated_select.html.haml | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 app/views/shared/_validated_select.html.haml diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index 6441cd619e..f5c10c3649 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -36,7 +36,18 @@ module CheckoutHelper "ng-class" => "{error: !fieldValid('#{path}')}" }.merge args - render partial: "shared/validated_input", locals: {name: name, path: path, attributes: attributes} + render "shared/validated_input", name: name, path: path, attributes: attributes + end + + def validated_select(name, path, options, args = {}) + attributes = { + required: true, + id: path, + "ng-model" => path, + "ng-class" => "{error: !fieldValid('#{path}')}" + }.merge args + + render "shared/validated_select", name: name, path: path, options: options, attributes: attributes end def reset_order diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index f9b42827db..c1cafa6161 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -39,8 +39,7 @@ = validated_input "City", "order.bill_address.city" .small-6.columns - = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, {include_blank: false}, - "ng-model" => "order.bill_address.state_id", required: true + = validated_select "State", "order.bill_address.state_id", [[]]+@order.billing_address.country.states.map{|c|[c.name, c.id]} .row .small-6.columns = validated_input "Postcode", "order.bill_address.zipcode" diff --git a/app/views/shared/_validated_select.html.haml b/app/views/shared/_validated_select.html.haml new file mode 100644 index 0000000000..35bdd971b3 --- /dev/null +++ b/app/views/shared/_validated_select.html.haml @@ -0,0 +1,6 @@ +%label{for: path}= name + += select_tag path, options_for_select(options), attributes + +%small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} + = "{{ fieldErrors('#{path}') }}" From 06a9110e95e730bd41c20a57f03d1a66c44c80e7 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 14:40:22 +1100 Subject: [PATCH 173/681] Extract checkout select options to helpers --- app/helpers/checkout_helper.rb | 8 ++++++++ app/views/checkout/_billing.html.haml | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index f5c10c3649..e47b393998 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -25,6 +25,14 @@ module CheckoutHelper order.display_item_total.money.to_f + checkout_adjustments_total(order).money.to_f end + def checkout_state_options + [[]] + @order.billing_address.country.states.map { |c| [c.name, c.id] } + end + + def checkout_country_options + available_countries.map { |c| [c.name, c.id] } + end + def validated_input(name, path, args = {}) attributes = { diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index c1cafa6161..07faa3d442 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -39,13 +39,13 @@ = validated_input "City", "order.bill_address.city" .small-6.columns - = validated_select "State", "order.bill_address.state_id", [[]]+@order.billing_address.country.states.map{|c|[c.name, c.id]} + = validated_select "State", "order.bill_address.state_id", checkout_state_options .row .small-6.columns = validated_input "Postcode", "order.bill_address.zipcode" .small-6.columns.right - = ba.select :country_id, available_countries.map{|c|[c.name, c.id]}, + = ba.select :country_id, checkout_country_options, "ng-model" => "order.bill_address.country_id", required: true .row From 7a0c291fc1de82168bab70d18ea3cb0a0946def3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 14:42:07 +1100 Subject: [PATCH 174/681] Checkout: Use validated_select for country --- app/views/checkout/_billing.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index 07faa3d442..14821d0f37 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -45,8 +45,7 @@ = validated_input "Postcode", "order.bill_address.zipcode" .small-6.columns.right - = ba.select :country_id, checkout_country_options, - "ng-model" => "order.bill_address.country_id", required: true + = validated_select "Country", "order.bill_address.country_id", checkout_country_options .row .small-12.columns.text-right From 433cf9dd9b0d58c725225659ad0a698bcadccc3c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 14:50:20 +1100 Subject: [PATCH 175/681] Checkout: Use validated_select for shipping address fields --- app/helpers/checkout_helper.rb | 10 ++++++++-- app/views/checkout/_billing.html.haml | 2 +- app/views/checkout/_shipping.html.haml | 6 ++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index e47b393998..1e672a0adc 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -25,8 +25,14 @@ module CheckoutHelper order.display_item_total.money.to_f + checkout_adjustments_total(order).money.to_f end - def checkout_state_options - [[]] + @order.billing_address.country.states.map { |c| [c.name, c.id] } + def checkout_state_options(source_address) + if source_address == :billing + address = @order.billing_address + elsif source_address == :shipping + address = @order.shipping_address + end + + [[]] + address.country.states.map { |c| [c.name, c.id] } end def checkout_country_options diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index 14821d0f37..c2573eef47 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -39,7 +39,7 @@ = validated_input "City", "order.bill_address.city" .small-6.columns - = validated_select "State", "order.bill_address.state_id", checkout_state_options + = validated_select "State", "order.bill_address.state_id", checkout_state_options(:billing) .row .small-6.columns = validated_input "Postcode", "order.bill_address.zipcode" diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index 9183f588f8..8e4ca9421b 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -66,14 +66,12 @@ .small-6.columns = validated_input "City", "order.ship_address.city" .small-6.columns - = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]}, {include_blank: false}, - "ng-model" => "order.ship_address.state_id", required: true + = validated_select "State", "order.ship_address.state_id", checkout_state_options(:shipping) .row .small-6.columns = validated_input "Postcode", "order.ship_address.zipcode" .small-6.columns.right - = sa.select :country_id, available_countries.map{|c|[c.name, c.id]}, - "ng-model" => "order.ship_address.country_id", required: true + = validated_select "Country", "order.ship_address.country_id", checkout_country_options .row .small-6.columns From 519aea2d9b8165780289ce693cbdbc2e376f775c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 15:19:33 +1100 Subject: [PATCH 176/681] Extract checkout shipping ship address to partial --- app/views/checkout/_shipping.html.haml | 30 ++----------------- .../checkout/_shipping_ship_address.html.haml | 28 +++++++++++++++++ 2 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 app/views/checkout/_shipping_ship_address.html.haml diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index 8e4ca9421b..e9d83816ac 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -48,34 +48,8 @@ = @order.order_cycle.pickup_time_for(@order.distributor) = f.fields_for :ship_address, @order.ship_address do |sa| - .small-12.columns - #ship_address{"ng-if" => "Checkout.requireShipAddress()"} - %div.visible{"ng-if" => "!Checkout.ship_address_same_as_billing"} - .row - .small-6.columns - = validated_input "First Name", "order.ship_address.firstname", "ofn-focus" => "accordion['shipping']" - .small-6.columns - = validated_input "Last Name", "order.ship_address.lastname" - .row - .small-12.columns - = validated_input "Address", "order.ship_address.address1" - .row - .small-12.columns - = validated_input "Address (contd.)", "order.ship_address.address2", required: false - .row - .small-6.columns - = validated_input "City", "order.ship_address.city" - .small-6.columns - = validated_select "State", "order.ship_address.state_id", checkout_state_options(:shipping) - .row - .small-6.columns - = validated_input "Postcode", "order.ship_address.zipcode" - .small-6.columns.right - = validated_select "Country", "order.ship_address.country_id", checkout_country_options - - .row - .small-6.columns - = validated_input "Phone", "order.ship_address.phone" + = render 'checkout/shipping_ship_address' + .row .small-12.columns = f.text_area :special_instructions, label: "Any notes or custom delivery instructions?", size: "60x4", "ng-model" => "order.special_instructions" diff --git a/app/views/checkout/_shipping_ship_address.html.haml b/app/views/checkout/_shipping_ship_address.html.haml new file mode 100644 index 0000000000..fa87eef2ff --- /dev/null +++ b/app/views/checkout/_shipping_ship_address.html.haml @@ -0,0 +1,28 @@ +.small-12.columns + #ship_address{"ng-if" => "Checkout.requireShipAddress()"} + %div.visible{"ng-if" => "!Checkout.ship_address_same_as_billing"} + .row + .small-6.columns + = validated_input "First Name", "order.ship_address.firstname", "ofn-focus" => "accordion['shipping']" + .small-6.columns + = validated_input "Last Name", "order.ship_address.lastname" + .row + .small-12.columns + = validated_input "Address", "order.ship_address.address1" + .row + .small-12.columns + = validated_input "Address (contd.)", "order.ship_address.address2", required: false + .row + .small-6.columns + = validated_input "City", "order.ship_address.city" + .small-6.columns + = validated_select "State", "order.ship_address.state_id", checkout_state_options(:shipping) + .row + .small-6.columns + = validated_input "Postcode", "order.ship_address.zipcode" + .small-6.columns.right + = validated_select "Country", "order.ship_address.country_id", checkout_country_options + + .row + .small-6.columns + = validated_input "Phone", "order.ship_address.phone" From f2c6ee87d3fc05395afdcb86a6613bf601e95849 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 15:20:06 +1100 Subject: [PATCH 177/681] Extract duplicated checkout accordion heading to partial --- .../checkout/payment_controller.js.coffee | 4 ++++ .../checkout/shipping_controller.js.coffee | 3 +++ .../checkout/_accordion_heading.html.haml | 15 +++++++++++++++ app/views/checkout/_billing.html.haml | 18 ++---------------- app/views/checkout/_details.html.haml | 16 +--------------- app/views/checkout/_payment.html.haml | 17 +---------------- app/views/checkout/_shipping.html.haml | 16 +--------------- 7 files changed, 27 insertions(+), 62 deletions(-) create mode 100644 app/views/checkout/_accordion_heading.html.haml diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee index 71f893aa64..1cfc724492 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee @@ -20,4 +20,8 @@ Darkswarm.controller "PaymentCtrl", ($scope, $timeout) -> $scope.years = [moment().year()..(moment().year()+15)] $scope.secrets.card_month = "1" $scope.secrets.card_year = moment().year() + + $scope.summary = -> + [$scope.Checkout.paymentMethod().name] + $timeout $scope.onTimeout diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee index cbb90b4352..429b974dd6 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee @@ -3,5 +3,8 @@ Darkswarm.controller "ShippingCtrl", ($scope, $timeout, ShippingMethods) -> $scope.ShippingMethods = ShippingMethods $scope.name = "shipping" $scope.nextPanel = "payment" + + $scope.summary = -> + [$scope.Checkout.shippingMethod().name] $timeout $scope.onTimeout diff --git a/app/views/checkout/_accordion_heading.html.haml b/app/views/checkout/_accordion_heading.html.haml new file mode 100644 index 0000000000..76724cd29e --- /dev/null +++ b/app/views/checkout/_accordion_heading.html.haml @@ -0,0 +1,15 @@ +%accordion-heading + .row + .small-8.medium-10.columns + %em + %small + {{ summary() | printArray }} + .small-4.medium-2.columns.text-right + %span.accordion-up + %em + %small Hide + %i.ofn-i_053-point-up + %span.accordion-down + %em + %small Expand + %i.ofn-i_052-point-down diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index c2573eef47..854f359b0c 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -11,22 +11,8 @@ %accordion-group{"is-open" => "accordion.billing", "ng-class" => "{valid: billing.$valid, open: accordion.billing}"} - %accordion-heading - .row - .small-8.medium-10.columns - %em - %small - {{ summary() | printArray }} - .small-4.medium-2.columns.text-right - %span.accordion-up - %em - %small Hide - %i.ofn-i_053-point-up - %span.accordion-down - %em - %small Expand - %i.ofn-i_052-point-down - + = render 'checkout/accordion_heading' + = f.fields_for :bill_address, @order.bill_address do |ba| .row .small-12.columns diff --git a/app/views/checkout/_details.html.haml b/app/views/checkout/_details.html.haml index 20407cbfd5..646a3312e0 100644 --- a/app/views/checkout/_details.html.haml +++ b/app/views/checkout/_details.html.haml @@ -11,21 +11,7 @@ %accordion-group{"is-open" => "accordion.details", "ng-class" => "{valid: details.$valid, open: accordion.details}"} - %accordion-heading - .row - .small-8.medium-10.columns - %em - %small - {{ summary() | printArray }} - .small-4.medium-2.columns.text-right - %span.accordion-up - %em - %small Hide - %i.ofn-i_053-point-up - %span.accordion-down - %em - %small Expand - %i.ofn-i_052-point-down + = render 'checkout/accordion_heading' .row .small-6.columns diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index e58e014f50..09e56c796e 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -11,22 +11,7 @@ %accordion-group{"is-open" => "accordion.payment", "ng-class" => "{valid: payment.$valid, open: accordion.payment}"} - %accordion-heading - .row - .small-8.medium-10.columns - %em - %small - {{ Checkout.paymentMethod().name }} - %small - .small-4.medium-2.columns.text-right - %span.accordion-up - %em - %small Hide - %i.ofn-i_053-point-up - %span.accordion-down - %em - %small Expand - %i.ofn-i_052-point-down + = render 'checkout/accordion_heading' -# TODO render this in Angular instead of server-side -# The problem being how to render the partials diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index e9d83816ac..22eee5f533 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -11,21 +11,7 @@ %accordion-group{"is-open" => "accordion.shipping", "ng-class" => "{valid: shipping.$valid, open: accordion.shipping}"} - %accordion-heading - .row - .small-8.medium-10.columns - %em - %small - {{ Checkout.shippingMethod().name }} - .small-4.medium-2.columns.text-right - %span.accordion-up - %em - %small Hide - %i.ofn-i_053-point-up - %span.accordion-down - %em - %small Expand - %i.ofn-i_052-point-down + = render 'checkout/accordion_heading' .small-12.columns.medium-6.columns.large-6.columns %label{"ng-repeat" => "method in ShippingMethods.shipping_methods"} From ca9da15ba42774d5cac7c3fb66c9403a79969036 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 16:01:07 +1100 Subject: [PATCH 178/681] Checkout: Fix shipping and payment method summary display when none selected --- .../darkswarm/controllers/checkout/payment_controller.js.coffee | 2 +- .../controllers/checkout/shipping_controller.js.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee index 1cfc724492..374f0b35d5 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee @@ -22,6 +22,6 @@ Darkswarm.controller "PaymentCtrl", ($scope, $timeout) -> $scope.secrets.card_year = moment().year() $scope.summary = -> - [$scope.Checkout.paymentMethod().name] + [$scope.Checkout.paymentMethod()?.name] $timeout $scope.onTimeout diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee index 429b974dd6..e2647e5761 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee @@ -5,6 +5,6 @@ Darkswarm.controller "ShippingCtrl", ($scope, $timeout, ShippingMethods) -> $scope.nextPanel = "payment" $scope.summary = -> - [$scope.Checkout.shippingMethod().name] + [$scope.Checkout.shippingMethod()?.name] $timeout $scope.onTimeout From 4389389d4401d2fa899cc5006815a2b3021c8101 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 16:09:18 +1100 Subject: [PATCH 179/681] Checkout: Display error when shipping or payment method is not selected --- app/views/checkout/_payment.html.haml | 4 ++++ app/views/checkout/_shipping.html.haml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index 09e56c796e..36775d6d35 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -23,9 +23,13 @@ %label = radio_button_tag "order[payments_attributes][][payment_method_id]", method.id, false, required: true, + name: "order.payment_method_id", "ng-model" => "order.payment_method_id" = method.name + %small.error.medium.input-text{"ng-show" => "!fieldValid('order.payment_method_id')"} + = "{{ fieldErrors('order.payment_method_id') }}" + .row{"ng-if" => "order.payment_method_id == #{method.id}"} .small-12.columns = render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index 22eee5f533..ffb1e43de4 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -17,10 +17,14 @@ %label{"ng-repeat" => "method in ShippingMethods.shipping_methods"} %input{type: :radio, required: true, + name: "order.shipping_method_id", "ng-value" => "method.id", "ng-model" => "order.shipping_method_id"} {{ method.name }} + %small.error.medium.input-text{"ng-show" => "!fieldValid('order.shipping_method_id')"} + = "{{ fieldErrors('order.shipping_method_id') }}" + %label{"ng-if" => "Checkout.requireShipAddress()"} %input{type: :checkbox, "ng-model" => "Checkout.ship_address_same_as_billing"} Shipping address same as billing address? From 57e7bc95041f75f24426ddf1a1728f3a6158e9df Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 16:45:21 +1100 Subject: [PATCH 180/681] Checkout: When error, open offending accordion section --- .../checkout/accordion_controller.js.coffee | 13 +++++++++++-- .../checkout/checkout_controller.js.coffee | 5 ++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee index b9bc76ced5..c2726f0d53 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee @@ -6,6 +6,15 @@ Darkswarm.controller "AccordionCtrl", ($scope, storage, $timeout, CurrentHub) -> billing: false storage.bind $scope, "accordion", {storeName: "accordion_#{$scope.order.id}#{CurrentHub.hub.id}#{$scope.order.user_id}"} - $scope.show = (name)-> - $scope.accordion[name] = true + $scope.show = (section)-> + $scope.accordion[section] = true + $scope.$on 'purchaseFormInvalid', (event, form) -> + # Scroll to first invalid section + # TODO: hide all first + # TODO: Use Object.keys($scope.accordion) + sections = ["details", "billing", "shipping", "payment"] + for section in sections + if not form[section].$valid + $scope.show section + break diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee index ed99c6bd41..950be00bae 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee @@ -21,4 +21,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, storage, Checkout, CurrentUser, Cu $scope.purchase = (event, form) -> event.preventDefault() $scope.submitted = true - $scope.Checkout.submit() if form.$valid + if form.$valid + $scope.Checkout.submit() + else + $scope.$broadcast 'purchaseFormInvalid', form From 3e151c40f59a754fe0ae296b65a90b460bf786de Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 6 Nov 2014 17:52:10 +1100 Subject: [PATCH 181/681] WIP: Smooth scroll to checkout errors --- .../checkout/accordion_controller.js.coffee | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee index c2726f0d53..d28437dd21 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee @@ -1,9 +1,10 @@ -Darkswarm.controller "AccordionCtrl", ($scope, storage, $timeout, CurrentHub) -> +Darkswarm.controller "AccordionCtrl", ($scope, storage, $timeout, $document, CurrentHub) -> $scope.accordion = details: true + billing: false shipping: false payment: false - billing: false + $scope.accordionSections = ["details", "billing", "shipping", "payment"] storage.bind $scope, "accordion", {storeName: "accordion_#{$scope.order.id}#{CurrentHub.hub.id}#{$scope.order.user_id}"} $scope.show = (section)-> @@ -11,10 +12,17 @@ Darkswarm.controller "AccordionCtrl", ($scope, storage, $timeout, CurrentHub) -> $scope.$on 'purchaseFormInvalid', (event, form) -> # Scroll to first invalid section - # TODO: hide all first - # TODO: Use Object.keys($scope.accordion) - sections = ["details", "billing", "shipping", "payment"] - for section in sections + for section in $scope.accordionSections if not form[section].$valid $scope.show section + + # If we call scrollTo() directly after show(), when one of the accordions above the + # scroll location is closed by show(), scrollTo() will scroll to the old location of + # the element. Putting this in a zero-length timeout is enough delay for the DOM to + # have updated. + $timeout -> + # Scrolling is confused by our position:fixed top bar - add an offset to scroll + # to the correct location, plus 5px buffer + offset_height = $("nav.top-bar").height() + 5 + $document.scrollTo $("##{section}"), offset_height, 500 break From 74642c3825d1ce89759bfb82ed7e26d196a00107 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 6 Nov 2014 21:18:20 +1100 Subject: [PATCH 182/681] Update checkout helper spec to reflect Rohan's changes --- spec/helpers/checkout_helper_spec.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/helpers/checkout_helper_spec.rb b/spec/helpers/checkout_helper_spec.rb index 6563ed8af9..c4979be40d 100644 --- a/spec/helpers/checkout_helper_spec.rb +++ b/spec/helpers/checkout_helper_spec.rb @@ -4,11 +4,12 @@ require 'spec_helper' describe CheckoutHelper do it "generates html for validated inputs" do helper.should_receive(:render).with( - partial: "shared/validated_input", - locals: {name: "test", path: "foo", - attributes: {:required=>true, :type=>:email, :name=>"foo", :id=>"foo", "ng-model"=>"foo", "ng-class"=>"{error: !fieldValid('foo')}"}} + "shared/validated_input", + name: "test", + path: "foo", + attributes: {:required=>true, :type=>:email, :name=>"foo", :id=>"foo", "ng-model"=>"foo", "ng-class"=>"{error: !fieldValid('foo')}"} ) - + helper.validated_input("test", "foo", type: :email) end end From 70c4b7082de79bdb2f104a10f8880d70c6dfface Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 31 Oct 2014 13:57:10 +1100 Subject: [PATCH 183/681] Split ent confirmation email out into layout and partial --- app/mailers/enterprise_mailer.rb | 2 + .../confirmation_instructions.html.haml | 118 +++--------------- app/views/layouts/mailer.html.haml | 56 +++++++++ .../mailers/_social_and_contact.html.haml | 29 +++++ 4 files changed, 106 insertions(+), 99 deletions(-) create mode 100644 app/views/layouts/mailer.html.haml create mode 100644 app/views/shared/mailers/_social_and_contact.html.haml diff --git a/app/mailers/enterprise_mailer.rb b/app/mailers/enterprise_mailer.rb index 4e37d112e0..b1edb43569 100644 --- a/app/mailers/enterprise_mailer.rb +++ b/app/mailers/enterprise_mailer.rb @@ -2,6 +2,8 @@ require 'devise/mailers/helpers' class EnterpriseMailer < Spree::BaseMailer include Devise::Mailers::Helpers + layout 'mailer' + def confirmation_instructions(record, token, opts={}) @token = token find_enterprise(record) diff --git a/app/views/enterprise_mailer/confirmation_instructions.html.haml b/app/views/enterprise_mailer/confirmation_instructions.html.haml index 19bab3f5ee..7d462a7ca2 100644 --- a/app/views/enterprise_mailer/confirmation_instructions.html.haml +++ b/app/views/enterprise_mailer/confirmation_instructions.html.haml @@ -1,100 +1,20 @@ -/ ORIGINAL & UGLY: -/ %p= "Welcome #{@resource.contact}!" -/ %p= "Please confirm your email address for #{@resource.name}." -/ %p= "Click the link below to activate your enterprise:" -/ %p= link_to 'Confirm this email address', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) +%h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} + = "Welcome, #{@resource.contact}!" +%p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} + = "Please confirm email address for your enterprise " + %strong + = "#{@resource.name}." +%p   +/ Callout Panel +%p.callout{:style => "margin: 0; padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + Click the link below to confirm email and to activate your enterprise. This link can be used only once: + %br + %strong + = link_to 'Confirm this email address »', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) +/ /Callout Panel +%p   +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + We're so excited that you're joining the Open Food Network! Don't hestitate to get in touch if you have any questions. +%p   - -%html{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :xmlns => "http://www.w3.org/1999/xhtml"} - %head{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - / If you delete this meta tag, Half Life 3 will never be released. - %meta{:content => "width=device-width", :name => "viewport", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ - %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ - %title{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} Open Food Network - %link{:href => "http://rohanmitchell.com/random/template/basic-email-template/stylesheets/email.css", :rel => "stylesheet", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :type => "text/css"}/ - %body{:bgcolor => "#FFFFFF", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-webkit-text-size-adjust: none;height: 100%;width: 100%!important;"} - / HEADER - %table.head-wrap{:bgcolor => "#333333", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td.header.container{:style => "margin: 0 auto!important;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block!important;max-width: 600px!important;clear: both!important;"} - .content{:style => "margin: 0 auto;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 600px;display: block;"} - %table{:bgcolor => "#333333", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %img{:src => "https://openfoodnetwork.org.au/assets/ofn_logo_beta-8e4dfc79deb25def2d107dea52dce492.png", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 100%;", :width => "200"}/ - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %h6.collapse{:style => "margin: 0!important;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #999;font-weight: 900;font-size: 14px;text-transform: uppercase;"} Open Food Network - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - / /HEADER - / BODY - %table.body-wrap{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td.container{:bgcolor => "#FFFFFF", :style => "margin: 0 auto!important;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block!important;max-width: 600px!important;clear: both!important;"} - .content{:style => "margin: 0 auto;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 600px;display: block;"} - %table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"}= "Welcome, #{@resource.contact}!" - %p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} - = "Please confirm email address for your enterprise " - %strong - = "#{@resource.name}." - %p   - / Callout Panel - %p.callout{:style => "margin: 0; padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - = "Click the link below to confirm email and to activate your enterprise. This link can be used only once:" - %br - %strong - = link_to 'Confirm this email address »', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) - / /Callout Panel - %p   - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"}= "We're so excited that you're joining the Open Food Network! Don't hestitate to get in touch if you have any questions." - %p   - / social & contact - %table.social{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;background-color: #ebebeb;width: 100%;", :width => "100%"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - / column 1 - %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %h5{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 900;font-size: 17px;"} Connect with Us: - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - %a.soc-btn.fb{:href => "https://www.facebook.com/OpenFoodNet", :style => "margin: 0;padding: 3px 7px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #FFF;font-size: 12px;margin-bottom: 10px;text-decoration: none;font-weight: bold;display: block;text-align: center;background-color: #3B5998!important;", :target => "_blank"} Facebook - %a.soc-btn.tw{:href => "https://twitter.com/OpenFoodNet", :style => "margin: 0;padding: 3px 7px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #FFF;font-size: 12px;margin-bottom: 10px;text-decoration: none;font-weight: bold;display: block;text-align: center;background-color: #1daced!important;", :target => "_blank"} Twitter - %a.soc-btn.li{:href => "http://www.linkedin.com/groups/Open-Food-Foundation-4743336", :style => "margin: 0;padding: 3px 7px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #FFF;font-size: 12px;margin-bottom: 10px;text-decoration: none;font-weight: bold;display: block;text-align: center;background-color: #0073b2!important;", :target => "_blank"} LinkedIn - / /column 1 - / column 2 - %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %h5{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 900;font-size: 17px;"} Email us: - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %a{:href => "hello@openfoodnetwork.org", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;"} hello@openfoodnetwork.org - / /column 2 - %span.clear{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block;clear: both;"} - / /social & contact - / /content - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - / /BODY - / FOOTER - %table.footer-wrap{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;clear: both!important;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td.container{:style => "margin: 0 auto!important;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block!important;max-width: 600px!important;clear: both!important;"} - / content - .content{:style => "margin: 0 auto;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 600px;display: block;"} - %table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:align => "center", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - %a{:href => "https://openfoodnetwork.org.au/Terms-of-service.pdf", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} Terms of service - | - %a{:href => "http://www.openfoodnetwork.org.au", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} Open Food Network - / | Unsubscribe - / /content - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - / /FOOTER += render 'shared/mailers/social_and_contact' diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml new file mode 100644 index 0000000000..74d05ad6b9 --- /dev/null +++ b/app/views/layouts/mailer.html.haml @@ -0,0 +1,56 @@ +%html{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :xmlns => "http://www.w3.org/1999/xhtml"} + %head{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + / If you delete this meta tag, Half Life 3 will never be released. + %meta{:content => "width=device-width", :name => "viewport", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ + %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ + %title{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} Open Food Network + %link{:href => "http://rohanmitchell.com/random/template/basic-email-template/stylesheets/email.css", :rel => "stylesheet", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :type => "text/css"}/ + %body{:bgcolor => "#FFFFFF", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-webkit-text-size-adjust: none;height: 100%;width: 100%!important;"} + / HEADER + %table.head-wrap{:bgcolor => "#333333", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td.header.container{:style => "margin: 0 auto!important;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block!important;max-width: 600px!important;clear: both!important;"} + .content{:style => "margin: 0 auto;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 600px;display: block;"} + %table{:bgcolor => "#333333", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %img{:src => "https://openfoodnetwork.org.au/assets/ofn_logo_beta-8e4dfc79deb25def2d107dea52dce492.png", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 100%;", :width => "200"}/ + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %h6.collapse{:style => "margin: 0!important;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #999;font-weight: 900;font-size: 14px;text-transform: uppercase;"} Open Food Network + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + / /HEADER + / BODY + %table.body-wrap{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td.container{:bgcolor => "#FFFFFF", :style => "margin: 0 auto!important;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block!important;max-width: 600px!important;clear: both!important;"} + .content{:style => "margin: 0 auto;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 600px;display: block;"} + %table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + + = yield + + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + / /BODY + / FOOTER + %table.footer-wrap{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;clear: both!important;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td.container{:style => "margin: 0 auto!important;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block!important;max-width: 600px!important;clear: both!important;"} + / content + .content{:style => "margin: 0 auto;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 600px;display: block;"} + %table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "center", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + %a{:href => "https://openfoodnetwork.org.au/Terms-of-service.pdf", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + Terms of service + | + %a{:href => "http://www.openfoodnetwork.org.au", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + Open Food Network + / | Unsubscribe + / /content + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + / /FOOTER \ No newline at end of file diff --git a/app/views/shared/mailers/_social_and_contact.html.haml b/app/views/shared/mailers/_social_and_contact.html.haml new file mode 100644 index 0000000000..50abe98ebd --- /dev/null +++ b/app/views/shared/mailers/_social_and_contact.html.haml @@ -0,0 +1,29 @@ +%table.social{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;background-color: #ebebeb;width: 100%;", :width => "100%"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + / column 1 + %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %h5{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 900;font-size: 17px;"} + Connect with Us: + %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + %a.soc-btn.fb{:href => "https://www.facebook.com/OpenFoodNet", :style => "margin: 0;padding: 3px 7px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #FFF;font-size: 12px;margin-bottom: 10px;text-decoration: none;font-weight: bold;display: block;text-align: center;background-color: #3B5998!important;", :target => "_blank"} + Facebook + %a.soc-btn.tw{:href => "https://twitter.com/OpenFoodNet", :style => "margin: 0;padding: 3px 7px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #FFF;font-size: 12px;margin-bottom: 10px;text-decoration: none;font-weight: bold;display: block;text-align: center;background-color: #1daced!important;", :target => "_blank"} + Twitter + %a.soc-btn.li{:href => "http://www.linkedin.com/groups/Open-Food-Foundation-4743336", :style => "margin: 0;padding: 3px 7px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #FFF;font-size: 12px;margin-bottom: 10px;text-decoration: none;font-weight: bold;display: block;text-align: center;background-color: #0073b2!important;", :target => "_blank"} + LinkedIn + / /column 1 + / column 2 + %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %h5{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 900;font-size: 17px;"} + Email us: + %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %a{:href => "hello@openfoodnetwork.org", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;"} + hello@openfoodnetwork.org + / /column 2 + %span.clear{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block;clear: both;"} From e6f5811a60079e080112c75df968001a92b1c4c2 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 31 Oct 2014 14:00:41 +1100 Subject: [PATCH 184/681] Only send confirmation instructions to the enterprise email address --- app/mailers/enterprise_mailer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/mailers/enterprise_mailer.rb b/app/mailers/enterprise_mailer.rb index b1edb43569..6ecb8c5799 100644 --- a/app/mailers/enterprise_mailer.rb +++ b/app/mailers/enterprise_mailer.rb @@ -9,7 +9,7 @@ class EnterpriseMailer < Spree::BaseMailer find_enterprise(record) opts = { subject: "Please confirm your email for #{@enterprise.name}", - to: [ @enterprise.owner.email, @enterprise.email ].uniq, + to: @enterprise.email, from: from_address, } devise_mail(record, :confirmation_instructions, opts) From 898af0a55ee3b4f51ff46faad6da6e10d70fe082 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 6 Nov 2014 13:59:06 +1100 Subject: [PATCH 185/681] Enterprise confirmation emails can be resent, and sensible things happen if confirmation link is clicked twice --- .../confirmations_controller_decorator.rb | 7 ---- .../enterprise_confirmations_controller.rb | 37 ++++++++++++++++ app/models/enterprise.rb | 2 +- app/models/spree/ability_decorator.rb | 2 +- .../single_enterprise_dashboard.html.haml | 1 + config/locales/en.yml | 6 +++ config/routes.rb | 2 +- .../devise/confirmation_controller_spec.rb | 15 ------- ...nterprise_confirmations_controller_spec.rb | 42 +++++++++++++++++++ 9 files changed, 89 insertions(+), 25 deletions(-) delete mode 100644 app/controllers/devise/confirmations_controller_decorator.rb create mode 100644 app/controllers/enterprise_confirmations_controller.rb delete mode 100644 spec/controllers/devise/confirmation_controller_spec.rb create mode 100644 spec/controllers/enterprise_confirmations_controller_spec.rb diff --git a/app/controllers/devise/confirmations_controller_decorator.rb b/app/controllers/devise/confirmations_controller_decorator.rb deleted file mode 100644 index ef34f28445..0000000000 --- a/app/controllers/devise/confirmations_controller_decorator.rb +++ /dev/null @@ -1,7 +0,0 @@ -Devise::ConfirmationsController.class_eval do - protected - # Override of devise method in Devise::ConfirmationsController - def after_confirmation_path_for(resource_name, resource) - spree.admin_path - end -end \ No newline at end of file diff --git a/app/controllers/enterprise_confirmations_controller.rb b/app/controllers/enterprise_confirmations_controller.rb new file mode 100644 index 0000000000..b9868c6f98 --- /dev/null +++ b/app/controllers/enterprise_confirmations_controller.rb @@ -0,0 +1,37 @@ +class EnterpriseConfirmationsController < DeviseController + include Spree::Core::ControllerHelpers::Auth # Needed for access to current_ability, so we can authorize! actions + + # GET /resource/confirmation/new + def new + build_resource({}) + end + + # POST /resource/confirmation + def create + self.resource = resource_class.find_by_unconfirmed_email_with_errors(resource_params) + authorize! :resend_confirmation, resource + + self.resource = resource_class.send_confirmation_instructions(resource_params) + + if successfully_sent?(resource) + set_flash_message(:success, :confirmation_sent) if is_navigational_format? + else + set_flash_message(:error, :confirmation_not_sent) if is_navigational_format? + end + + respond_with_navigational(resource){ redirect_to spree.admin_path } + end + + # GET /resource/confirmation?confirmation_token=abcdef + def show + self.resource = resource_class.confirm_by_token(params[:confirmation_token]) + + if resource.errors.empty? + set_flash_message(:success, :confirmed) if is_navigational_format? + else + set_flash_message(:error, :not_confirmed) if is_navigational_format? + end + + respond_with_navigational(resource){ redirect_to spree.admin_path } + end +end \ No newline at end of file diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 0c98bcc7bf..b5066264e1 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -3,7 +3,7 @@ class Enterprise < ActiveRecord::Base SHOP_TRIAL_LENGTH = 30 ENTERPRISE_SEARCH_RADIUS = 100 - devise :confirmable, reconfirmable: true + devise :confirmable, reconfirmable: true, confirmation_keys: [ :id, :email ] self.inheritance_column = nil diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 23994a4580..3a0466f1cc 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -51,7 +51,7 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty can [:admin, :index, :create], Enterprise - can [:read, :edit, :update, :bulk_update, :set_sells], Enterprise do |enterprise| + can [:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], Enterprise do |enterprise| user.enterprises.include? enterprise end diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index e88fe4eb11..6f48b73fa2 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -31,6 +31,7 @@ %strong= "#{@enterprise.name}." We've sent an email to %strong= "#{@enterprise.email}." + = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.email } ), method: :post) %a.close{ href: "#" } × - if !@enterprise.visible .alert-box diff --git a/config/locales/en.yml b/config/locales/en.yml index 378e411406..e1aadad6b8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,5 +7,11 @@ en: invalid: | Invalid email or password. Were you a guest last time? Perhaps you need to create an account or reset your password. + enterprise_confirmations: + enterprise: + confirmed: Thankyou, your email address has been confirmed. + not_confirmed: Your email address could not be confirmed. Perhaps you have already completed this step? + confirmation_sent: "Confirmation email sent!" + confirmation_not_sent: "Could not send a confirmation email." home: "OFN" search_by_name: Search by name or suburb... diff --git a/config/routes.rb b/config/routes.rb index 62f55065fc..6dfe7cc900 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,7 +34,7 @@ Openfoodnetwork::Application.routes.draw do end end - devise_for :enterprise + devise_for :enterprise, controllers: { confirmations: 'enterprise_confirmations' } namespace :admin do resources :order_cycles do diff --git a/spec/controllers/devise/confirmation_controller_spec.rb b/spec/controllers/devise/confirmation_controller_spec.rb deleted file mode 100644 index b8719bbd8f..0000000000 --- a/spec/controllers/devise/confirmation_controller_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe Devise::ConfirmationsController do - context "after confirmation" do - before do - e = create(:enterprise, confirmed_at: nil) - @request.env["devise.mapping"] = Devise.mappings[:enterprise] - spree_get :show, confirmation_token: e.confirmation_token - end - - it "should redirect to admin root" do - expect(response).to redirect_to spree.admin_path - end - end -end \ No newline at end of file diff --git a/spec/controllers/enterprise_confirmations_controller_spec.rb b/spec/controllers/enterprise_confirmations_controller_spec.rb new file mode 100644 index 0000000000..fa7746df36 --- /dev/null +++ b/spec/controllers/enterprise_confirmations_controller_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe EnterpriseConfirmationsController do + include AuthenticationWorkflow + let!(:user) { create_enterprise_user( enterprise_limit: 10 ) } + let!(:unconfirmed_enterprise) { create(:distributor_enterprise, confirmed_at: nil, owner: user) } + let!(:confirmed_enterprise) { create(:distributor_enterprise, owner: user) } + let!(:unowned_enterprise) { create(:distributor_enterprise) } + + before do + controller.stub spree_current_user: user + @request.env["devise.mapping"] = Devise.mappings[:enterprise] + end + + context "confirming an enterprise" do + it "that has already been confirmed" do + spree_get :show, confirmation_token: confirmed_enterprise.confirmation_token + expect(response).to redirect_to spree.admin_path + expect(flash[:error]).to eq I18n.t('devise.enterprise_confirmations.enterprise.not_confirmed') + end + + it "that has not already been confirmed" do + spree_get :show, confirmation_token: unconfirmed_enterprise.confirmation_token + expect(response).to redirect_to spree.admin_path + expect(flash[:success]).to eq I18n.t('devise.enterprise_confirmations.enterprise.confirmed') + end + end + + context "requesting confirmation instructions to be resent" do + it "when the user owns the enterprise" do + spree_post :create, { enterprise: { id: unconfirmed_enterprise.id, email: unconfirmed_enterprise.email } } + expect(response).to redirect_to spree.admin_path + expect(flash[:success]).to eq I18n.t('devise.enterprise_confirmations.enterprise.confirmation_sent') + end + + it "when the user does not own the enterprise" do + spree_post :create, { enterprise: { id: unowned_enterprise.id, email: unowned_enterprise.email } } + expect(response).to redirect_to spree.unauthorized_path + expect(flash[:error]).to eq "Authorization Failure" + end + end +end \ No newline at end of file From e1823212d6adbb4f74979b622fc54b14274f8e6b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 6 Nov 2014 15:19:46 +1100 Subject: [PATCH 186/681] Reconfirmation email not required when we already know about new email address for enterprise --- app/models/enterprise.rb | 11 +++++++---- spec/models/enterprise_spec.rb | 22 ++++++++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index b5066264e1..42fc965613 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -9,8 +9,6 @@ class Enterprise < ActiveRecord::Base acts_as_gmappable :process_geocoding => false - before_create :check_email - has_and_belongs_to_many :groups, class_name: 'EnterpriseGroup' has_many :producer_properties, foreign_key: 'producer_id' has_many :supplied_products, :class_name => 'Spree::Product', :foreign_key => 'supplier_id', :dependent => :destroy @@ -57,6 +55,8 @@ class Enterprise < ActiveRecord::Base validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? } validates_length_of :description, :maximum => 255 + before_save :email_check, if: lambda{ email_changed? } + before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? } before_validation :set_unused_address_fields after_validation :geocode_address @@ -299,8 +299,11 @@ class Enterprise < ActiveRecord::Base private - def check_email - skip_confirmation! if owner.enterprises.confirmed.map(&:email).include?(email) + def email_check + # Skip confirmation/reconfirmation if the new email has already been confirmed + if owner.enterprises.confirmed.map(&:email).include?(email) + new_record? ? skip_confirmation! : skip_reconfirmation! + end end def strip_url(url) diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 4fa6e2b3f6..c477ccda68 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -10,16 +10,34 @@ describe Enterprise do it "when the email address has not already been confirmed" do mail_message = double "Mail::Message" - EnterpriseMailer.should_receive(:confirmation_instructions).and_return mail_message + expect(EnterpriseMailer).to receive(:confirmation_instructions).and_return mail_message mail_message.should_receive :deliver create(:enterprise, owner: user, email: "unknown@email.com", confirmed_at: nil ) end it "when the email address has already been confirmed" do - EnterpriseMailer.should_not_receive(:confirmation_instructions) + expect(EnterpriseMailer).to_not receive(:confirmation_instructions) e = create(:enterprise, owner: user, email: enterprise.email, confirmed_at: nil) end end + + describe "on update of email" do + let!(:user) { create_enterprise_user( enterprise_limit: 2 ) } + let!(:enterprise) { create(:enterprise, owner: user) } + + it "when the email address has not already been confirmed" do + mail_message = double "Mail::Message" + expect(EnterpriseMailer).to receive(:confirmation_instructions).and_return mail_message + mail_message.should_receive :deliver + enterprise.update_attributes(email: "unknown@email.com") + end + + it "when the email address has already been confirmed" do + create(:enterprise, owner: user, email: "second.known.email@email.com") # Another enterpise with same owner but different email + expect(EnterpriseMailer).to_not receive(:confirmation_instructions) + enterprise.update_attributes!(email: "second.known.email@email.com") + end + end end describe "associations" do From cd34aedbb90462e4dd7b2d0d1716cbc51c5ccffb Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 7 Nov 2014 08:48:24 +1100 Subject: [PATCH 187/681] New partial for producer register pricing table --- app/views/home/_producer-register.html.haml | 72 +++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 app/views/home/_producer-register.html.haml diff --git a/app/views/home/_producer-register.html.haml b/app/views/home/_producer-register.html.haml new file mode 100644 index 0000000000..59270cee6e --- /dev/null +++ b/app/views/home/_producer-register.html.haml @@ -0,0 +1,72 @@ +#producers.pane + + .row + .small-12.columns.text-center + %h2 Aussie Producers + %h5 Want to join the Open Food Network? + %br + / %a.neutral-btn.turquoise{href: "/register"} + / Register now + / %i.ofn-i_007-caret-right + .row + .small-12.medium-4.columns.text-center + %ul.pricing-table + %li.title Profile + %li.price Always free + %li.description Help people find and contact you on OFN + %li.bullet-item + %i.ofn-i_019-map-pin + Pin on OFN Map + %li.bullet-item + %i.ofn-i_044-facebook + Share your contact and social info + %li.cta-button + %a.neutral-btn.turquoise{:href => "/register"} Register now + + .small-12.medium-4.columns.text-center + %ul.pricing-table + %li.title Supplier + %li.price Always free + %li.description Sell your products through existing OFN shopfronts + %li.bullet-item + %i.ofn-i_019-map-pin + Pin on OFN Map + %li.bullet-item + %i.ofn-i_044-facebook + Share your contact and social info + %li.bullet-item + %i.ofn-i_067-shop + Create and manage products + %li.cta-button + %a.neutral-btn.turquoise{:href => "/register"} Register now + + .small-12.medium-4.columns.text-center + %ul.pricing-table + %li.title Shopfront + %li.price $200 setup fee + %li.description + Sliding monthly fee of $5-$50/month + %li.bullet-item + %i.ofn-i_019-map-pin + Pin on OFN Map + %li.bullet-item + %i.ofn-i_044-facebook + Share your contact and social info + %li.bullet-item + %i.ofn-i_067-shop + Create and manage products + / %li.description Monthly fee is a percentage of your shopfront earnings, minimum $5, capped at $50 + %li.bullet-item + %i.ofn-i_051-check-big + Create and manage order cycles + %li.bullet-item + %i.ofn-i_051-check-big + Sell your products on Shopfront + %li.cta-button + %a.neutral-btn.turquoise{:href => "/register"} Register now + + / %p + / Having a profile is always free! + / %br + / If you want your own OFN Shopfront, pricing scales to match your incoming - capped at $50/month. + + \ No newline at end of file From 9a2e510f2c69fd6dd036a3e1382458613894ae6a Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 7 Nov 2014 08:48:37 +1100 Subject: [PATCH 188/681] Add Producers register link to footer --- app/views/shared/_footer.html.haml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index c5f44988d6..f3411dd3e9 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -17,7 +17,7 @@ %a{title:'Join our group on LinkedIn', href: 'http://www.linkedin.com/groups/Open-Food-Foundation-4743336', target: '_blank'} %i.ofn-i_042-linkedin LinkedIn - .small-12.medium-4.columns.text-left + .small-12.medium-3.columns.text-left %h4 Getting around %ul.bullet-list %li @@ -28,7 +28,12 @@ %a{href: "/producers"} Producers %li %a{href: "/groups"} Groups - .small-12.medium-4.columns.text-left + .small-12.medium-2.columns.text-left + %h4 Producers + %p All Australian producers are now welcome to join the Open Food Network. + %p + %a{href: "/register"} Register now + .small-12.medium-3.columns.text-left %h4 About us %p OFN is a network of independent online food stores that connect farmers and food hubs with individuals and local businesses. It gives farmers and food hubs an easier and fairer way to distribute their food. .row.landing-page-row From 039b94aa470486ccfe06492221bdf59b50385b9a Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 7 Nov 2014 08:48:54 +1100 Subject: [PATCH 189/681] Styling for Producers Register pane and pricing table --- .../stylesheets/darkswarm/home_panes.css.sass | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/home_panes.css.sass b/app/assets/stylesheets/darkswarm/home_panes.css.sass index 339c74644c..5ef57f90c0 100644 --- a/app/assets/stylesheets/darkswarm/home_panes.css.sass +++ b/app/assets/stylesheets/darkswarm/home_panes.css.sass @@ -32,11 +32,23 @@ #producers.pane @include turqbg + @include panepadding + background-image: url("/assets/home/producers-bg.svg") + background-repeat: no-repeat + background-position: center 80px + background-size: 80% 80% + + @media all and (max-width: 768px) + background-position: center top + background-size: 100% 100% .row - @include panepadding - background-image: url("/assets/home/producers-bg.svg") - background-repeat: no-repeat - background-position: right center + .pricing-table + .title + color: $clr-turquoise-light + .price + background-color: rgba(240, 240, 240, 0.6) + .description, .bullet-item, .cta-button + background-color: rgba(255, 255, 255, 0.8) // Responsive @media all and (max-width: 768px) From 4795de5ab0464e1ce85eb04fb81bcd233c24a9f1 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 7 Nov 2014 08:49:09 +1100 Subject: [PATCH 190/681] Adding new partial into homepage --- app/views/home/index.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index da6dc97b02..a15442e794 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -17,6 +17,8 @@ / = render partial: "home/groups" += render partial: "home/producer-register" + = render partial: "home/beta" = render partial: "shared/footer" From 9f74e8ff035146364bfe6ba83580ba09c5eb5b69 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 7 Nov 2014 12:05:35 +1100 Subject: [PATCH 191/681] Add caret to right on register buttons, kill commented out text --- app/views/home/_producer-register.html.haml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/views/home/_producer-register.html.haml b/app/views/home/_producer-register.html.haml index 59270cee6e..9be76ae2ef 100644 --- a/app/views/home/_producer-register.html.haml +++ b/app/views/home/_producer-register.html.haml @@ -21,7 +21,9 @@ %i.ofn-i_044-facebook Share your contact and social info %li.cta-button - %a.neutral-btn.turquoise{:href => "/register"} Register now + %a.neutral-btn.turquoise{:href => "/register"} + Register now + %i.ofn-i_007-caret-right .small-12.medium-4.columns.text-center %ul.pricing-table @@ -38,7 +40,9 @@ %i.ofn-i_067-shop Create and manage products %li.cta-button - %a.neutral-btn.turquoise{:href => "/register"} Register now + %a.neutral-btn.turquoise{:href => "/register"} + Register now + %i.ofn-i_007-caret-right .small-12.medium-4.columns.text-center %ul.pricing-table @@ -62,11 +66,10 @@ %i.ofn-i_051-check-big Sell your products on Shopfront %li.cta-button - %a.neutral-btn.turquoise{:href => "/register"} Register now - - / %p - / Having a profile is always free! - / %br - / If you want your own OFN Shopfront, pricing scales to match your incoming - capped at $50/month. + %a.neutral-btn.turquoise{:href => "/register"} + Register now + %i.ofn-i_007-caret-right - \ No newline at end of file + + + \ No newline at end of file From 2166fbc8a0b94e206043aafcc2162778ad310c20 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 7 Nov 2014 13:56:07 +1100 Subject: [PATCH 192/681] Tweak wording for kirsten --- app/views/shared/_footer.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index f3411dd3e9..3791bd130d 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -30,7 +30,7 @@ %a{href: "/groups"} Groups .small-12.medium-2.columns.text-left %h4 Producers - %p All Australian producers are now welcome to join the Open Food Network. + %p Australian producers are now welcome to join the Open Food Network. %p %a{href: "/register"} Register now .small-12.medium-3.columns.text-left From 791cc9b3011f9321ec8cf444c56f1d4ee3f5d02d Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 7 Nov 2014 13:56:28 +1100 Subject: [PATCH 193/681] Hide pricing table until language is sorted --- app/views/home/_producer-register.html.haml | 125 ++++++++++---------- 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/app/views/home/_producer-register.html.haml b/app/views/home/_producer-register.html.haml index 9be76ae2ef..dd92974705 100644 --- a/app/views/home/_producer-register.html.haml +++ b/app/views/home/_producer-register.html.haml @@ -4,71 +4,70 @@ .small-12.columns.text-center %h2 Aussie Producers %h5 Want to join the Open Food Network? - %br - / %a.neutral-btn.turquoise{href: "/register"} - / Register now - / %i.ofn-i_007-caret-right - .row - .small-12.medium-4.columns.text-center - %ul.pricing-table - %li.title Profile - %li.price Always free - %li.description Help people find and contact you on OFN - %li.bullet-item - %i.ofn-i_019-map-pin - Pin on OFN Map - %li.bullet-item - %i.ofn-i_044-facebook - Share your contact and social info - %li.cta-button - %a.neutral-btn.turquoise{:href => "/register"} - Register now - %i.ofn-i_007-caret-right + %br + %a.neutral-btn.turquoise{href: "/register"} + Register now + %i.ofn-i_007-caret-right + / .row + / .small-12.medium-4.columns.text-center + / %ul.pricing-table + / %li.title Profile + / %li.price Always free + / %li.description Help people find and contact you on OFN + / %li.bullet-item + / %i.ofn-i_019-map-pin + / Pin on OFN Map + / %li.bullet-item + / %i.ofn-i_044-facebook + / Share your contact and social info + / %li.cta-button + / %a.neutral-btn.turquoise{:href => "/register"} + / Register now + / %i.ofn-i_007-caret-right - .small-12.medium-4.columns.text-center - %ul.pricing-table - %li.title Supplier - %li.price Always free - %li.description Sell your products through existing OFN shopfronts - %li.bullet-item - %i.ofn-i_019-map-pin - Pin on OFN Map - %li.bullet-item - %i.ofn-i_044-facebook - Share your contact and social info - %li.bullet-item - %i.ofn-i_067-shop - Create and manage products - %li.cta-button - %a.neutral-btn.turquoise{:href => "/register"} - Register now - %i.ofn-i_007-caret-right + / .small-12.medium-4.columns.text-center + / %ul.pricing-table + / %li.title Supplier + / %li.price Always free + / %li.description Sell your products through existing OFN shopfronts + / %li.bullet-item + / %i.ofn-i_019-map-pin + / Pin on OFN Map + / %li.bullet-item + / %i.ofn-i_044-facebook + / Share your contact and social info + / %li.bullet-item + / %i.ofn-i_067-shop + / Create and manage products + / %li.cta-button + / %a.neutral-btn.turquoise{:href => "/register"} + / Register now + / %i.ofn-i_007-caret-right - .small-12.medium-4.columns.text-center - %ul.pricing-table - %li.title Shopfront - %li.price $200 setup fee - %li.description + Sliding monthly fee of $5-$50/month - %li.bullet-item - %i.ofn-i_019-map-pin - Pin on OFN Map - %li.bullet-item - %i.ofn-i_044-facebook - Share your contact and social info - %li.bullet-item - %i.ofn-i_067-shop - Create and manage products - / %li.description Monthly fee is a percentage of your shopfront earnings, minimum $5, capped at $50 - %li.bullet-item - %i.ofn-i_051-check-big - Create and manage order cycles - %li.bullet-item - %i.ofn-i_051-check-big - Sell your products on Shopfront - %li.cta-button - %a.neutral-btn.turquoise{:href => "/register"} - Register now - %i.ofn-i_007-caret-right + / .small-12.medium-4.columns.text-center + / %ul.pricing-table + / %li.title Shopfront + / %li.price $200 setup fee + / %li.description + Sliding monthly fee of $5-$50/month + / %li.bullet-item + / %i.ofn-i_019-map-pin + / Pin on OFN Map + / %li.bullet-item + / %i.ofn-i_044-facebook + / Share your contact and social info + / %li.bullet-item + / %i.ofn-i_067-shop + / Create and manage products + / %li.bullet-item + / %i.ofn-i_051-check-big + / Create and manage order cycles + / %li.bullet-item + / %i.ofn-i_051-check-big + / Sell your products on Shopfront + / %li.cta-button + / %a.neutral-btn.turquoise{:href => "/register"} + / Register now + / %i.ofn-i_007-caret-right From 2addb1c4720df3bef4d3c75560a2bdfd8e2a7591 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 7 Nov 2014 13:56:56 +1100 Subject: [PATCH 194/681] Tweak producer register pane styling for lightweight view --- app/assets/stylesheets/darkswarm/home_panes.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/home_panes.css.sass b/app/assets/stylesheets/darkswarm/home_panes.css.sass index 5ef57f90c0..568b6ebc66 100644 --- a/app/assets/stylesheets/darkswarm/home_panes.css.sass +++ b/app/assets/stylesheets/darkswarm/home_panes.css.sass @@ -35,7 +35,7 @@ @include panepadding background-image: url("/assets/home/producers-bg.svg") background-repeat: no-repeat - background-position: center 80px + background-position: center center background-size: 80% 80% @media all and (max-width: 768px) From 66bbaabf7977dde9a5c1e1a00cf3d700600de24c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 7 Nov 2014 16:50:31 +1100 Subject: [PATCH 195/681] Adding loading gif to shop page --- .../stylesheets/darkswarm/shop.css.sass | 29 ++++--------------- app/views/shop/products/_form.html.haml | 7 +++-- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 083defca77..6e5f397475 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -10,17 +10,14 @@ @import shop-taxon-flag @import shop-popovers - - .darkswarm products display: block padding-top: 2.3em - @media all and (max-width: 768px) padding-top: 1em - + input.button.right float: left @@ -28,7 +25,6 @@ .add_to_cart margin-top: 2rem - product @include csstrans border-bottom: 1px solid #e5e5e5 @@ -55,12 +51,14 @@ .inline display: inline - + .spinner + width: 100px + // ICONS i font-size: 0.75em padding-right: 0.9375rem - @media all and (max-width: 640px) + @media all and (max-width: 640px) padding-right: 0.25rem i.ofn-i_056-bulk, i.ofn-i_036-producers @@ -69,20 +67,3 @@ i.ofn-i_036-producers padding-left: 0.5rem - - - - - - - - - - - - - - - - - diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 16e003c83e..ef44a133eb 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -3,7 +3,7 @@ .row .small-12.medium-8.large-9.columns - %input#search.text{"ng-model" => "query", + %input#search.text{"ng-model" => "query", placeholder: "Search by product or producer", "ng-debounce" => "100", "ofn-disable-enter" => true} @@ -22,11 +22,14 @@ = render partial: "shop/products/summary" %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants"} %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id"} - + %product{"ng-show" => "Products.loading"} .row.summary .small-12.columns.text-center Loading products + .row + .small-12.columns.text-center + %img.spinner{ src: "/assets/loading.gif" } %div{"ng-show" => "filteredProducts.length == 0 && !Products.loading"} .row.summary From 0432fe229beb415c431483131847f3e2264762d9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 7 Nov 2014 16:53:15 +1100 Subject: [PATCH 196/681] Add bottom margin to loading gif --- app/assets/stylesheets/darkswarm/shop.css.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 6e5f397475..554d3f2f4e 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -53,6 +53,7 @@ .spinner width: 100px + margin-bottom: 20px // ICONS i From 1d0dab5cc6b5f5841819fb83ade8b25c7f97b373 Mon Sep 17 00:00:00 2001 From: Paul Mackay Date: Sun, 9 Nov 2014 12:08:40 +0000 Subject: [PATCH 197/681] Small doc tweak to add db:setup command. --- README.markdown | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 6ffde4df5c..2710aa1162 100644 --- a/README.markdown +++ b/README.markdown @@ -48,7 +48,11 @@ Install the project's gem dependencies: bundle install -Create the development and test databases, using the settings specified in `config/database.yml`. You can then load the schema and some seed data with the following command: +Create the development and test databases, using the settings specified in `config/database.yml`: + + rake db:setup + +Then load the schema and some seed data with the following command: rake db:schema:load db:seed From bb9b244d18a03356372e9c6dc7aeee7c08919afc Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 10 Nov 2014 14:54:49 +1100 Subject: [PATCH 198/681] Upgrade paperclip to 3.x, fixes incorrect cropping of EXIF-rotated JPGs --- Gemfile.lock | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c1a2c66522..b98ceb4789 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,7 +23,7 @@ GIT GIT remote: git://github.com/openfoodfoundation/spree.git - revision: bbe5e779bcb883a1726ad4006d7c06b06c3f5372 + revision: 4e0075b07acb56864aca89eee3d9670136176c23 branch: 1-3-stable specs: spree (1.3.6.beta) @@ -50,7 +50,7 @@ GIT json (>= 1.5.5) kaminari (= 0.14.1) money (= 5.1.1) - paperclip (~> 2.8) + paperclip (~> 3.0) rabl (= 0.7.2) rails (~> 3.2.16) ransack (= 0.7.2) @@ -121,17 +121,17 @@ GEM active_link_to (1.0.0) active_model_serializers (0.8.1) activemodel (>= 3.0) - active_utils (2.2.1) + active_utils (2.2.3) activesupport (>= 2.3.11) i18n - activemerchant (1.43.1) - active_utils (~> 2.0, >= 2.0.1) - activesupport (>= 2.3.14, < 5.0.0) + activemerchant (1.44.1) + active_utils (~> 2.2.0) + activesupport (>= 3.2.14, < 5.0.0) builder (>= 2.1.2, < 4.0.0) - i18n (~> 0.5) + i18n (>= 0.6.9) json (~> 1.7) - money (< 7.0.0) nokogiri (~> 1.4) + offsite_payments (~> 2.0.0) activemodel (3.2.19) activesupport (= 3.2.19) builder (~> 3.0.0) @@ -292,7 +292,7 @@ GEM httparty (0.13.1) json (~> 1.8) multi_xml (>= 0.5.2) - i18n (0.6.9) + i18n (0.6.11) immigrant (0.1.6) activerecord (>= 3.0) foreigner (>= 1.2.1) @@ -323,7 +323,7 @@ GEM treetop (~> 1.4.8) method_source (0.8.1) mime-types (1.25.1) - mini_portile (0.6.0) + mini_portile (0.6.1) momentjs-rails (2.5.1) railties (>= 3.1) money (5.1.1) @@ -334,14 +334,22 @@ GEM net-ssh (>= 2.6.5) net-ssh (2.6.8) newrelic_rpm (3.6.7.152) - nokogiri (1.6.2.1) - mini_portile (= 0.6.0) + nokogiri (1.6.4.1) + mini_portile (~> 0.6.0) + offsite_payments (2.0.1) + active_utils (~> 2.2.0) + activesupport (>= 3.2.14, < 5.0.0) + builder (>= 2.1.2, < 4.0.0) + i18n (~> 0.5) + json (~> 1.7) + money (< 7.0.0) + nokogiri (~> 1.4) oj (2.1.2) orm_adapter (0.5.0) - paperclip (2.8.0) - activerecord (>= 2.3.0) - activesupport (>= 2.3.2) - cocaine (>= 0.0.2) + paperclip (3.5.4) + activemodel (>= 3.0.0) + activesupport (>= 3.0.0) + cocaine (~> 0.5.3) mime-types paypal-sdk-core (0.2.10) multi_json (~> 1.0) @@ -445,7 +453,7 @@ GEM slop (3.4.5) spinjs-rails (1.3) rails (>= 3.1) - sprockets (2.2.2) + sprockets (2.2.3) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) @@ -468,7 +476,7 @@ GEM sprockets (>= 2.0.0) turn (0.8.3) ansi - tzinfo (0.3.39) + tzinfo (0.3.42) uglifier (1.2.4) execjs (>= 0.3.0) multi_json (>= 1.0.2) @@ -479,7 +487,7 @@ GEM unicorn-rails (1.1.0) rack unicorn - uuidtools (2.1.4) + uuidtools (2.1.5) versioncake (0.4.0) actionpack (>= 3.0) activesupport (>= 3.0) From 9a5ee81431d7a16219250277aef239d6973c1ff5 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Tue, 11 Nov 2014 17:49:08 +0000 Subject: [PATCH 199/681] Changing report download titles in include download date --- .../spree/admin/reports_controller_decorator.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 22dd41549f..8dfd5e0708 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -50,7 +50,7 @@ Spree::Admin::ReportsController.class_eval do @report_type = params[:report_type] @report = OpenFoodNetwork::CustomersReport.new spree_current_user, params - render_report(@report.header, @report.table, params[:csv], "customers.csv") + render_report(@report.header, @report.table, params[:csv], "customers_"+Time.now.strftime("%Y%m%d")+".csv") end def orders_and_distributors @@ -78,7 +78,7 @@ Spree::Admin::ReportsController.class_eval do csv << @report.header @report.table.each { |row| csv << row } end - send_data csv_string, :filename => "orders_and_distributors.csv" + send_data csv_string, :filename => "orders_and_distributors_"+Time.now.strftime("%Y%m%d")+".csv" end end @@ -230,7 +230,7 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(@line_items) - csv_file_name = "bulk_coop.csv" + csv_file_name = "bulk_coop_"+Time.now.strftime("%Y%m%d")+".csv" render_report(@header, @table, params[:csv], csv_file_name) end @@ -334,7 +334,7 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(table_items) - csv_file_name = "payments.csv" + csv_file_name = "payments_"+Time.now.strftime("%Y%m%d")+".csv" render_report(@header, @table, params[:csv], csv_file_name) @@ -564,7 +564,7 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(table_items) - csv_file_name = "#{__method__}.csv" + csv_file_name = params[:report_type]+"_"+Time.now.strftime("%Y%m%d")+".csv" render_report(@header, @table, params[:csv], csv_file_name) @@ -576,7 +576,7 @@ Spree::Admin::ReportsController.class_eval do @report = OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params #@table = @report.table #@header = @report.header - render_report(@report.header, @report.table, params[:csv], "products_and_inventory.csv") + render_report(@report.header, @report.table, params[:csv], "products_and_inventory_"+Time.now.strftime("%Y%m%d")+".csv") end def render_report (header, table, create_csv, csv_file_name) From ee2ee5dba72992cb1a2c23375dafa7cc057663ab Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 12 Nov 2014 11:57:23 +1100 Subject: [PATCH 200/681] Adding an enterprise welcome email --- app/mailers/enterprise_mailer.rb | 6 ++ app/models/enterprise.rb | 26 +++++++- .../confirmation_instructions.html.haml | 2 +- app/views/enterprise_mailer/welcome.html.haml | 25 ++++++++ spec/mailers/enterprise_mailer_spec.rb | 7 +++ spec/models/enterprise_spec.rb | 61 ++++++++++++++++--- 6 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 app/views/enterprise_mailer/welcome.html.haml diff --git a/app/mailers/enterprise_mailer.rb b/app/mailers/enterprise_mailer.rb index 6ecb8c5799..2e6e83d92c 100644 --- a/app/mailers/enterprise_mailer.rb +++ b/app/mailers/enterprise_mailer.rb @@ -4,6 +4,12 @@ class EnterpriseMailer < Spree::BaseMailer layout 'mailer' + def welcome(enterprise) + @enterprise = enterprise + mail(:to => enterprise.email, :from => from_address, + :subject => "#{enterprise.name} is now on #{Spree::Config[:site_name]}") + end + def confirmation_instructions(record, token, opts={}) @token = token find_enterprise(record) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 42fc965613..8d4f688565 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -55,12 +55,16 @@ class Enterprise < ActiveRecord::Base validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? } validates_length_of :description, :maximum => 255 - before_save :email_check, if: lambda{ email_changed? } + before_save :confirmation_check, if: lambda{ email_changed? } before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? } before_validation :set_unused_address_fields after_validation :geocode_address + # TODO: Later versions of devise have a dedicated after_confirmation callback, so use that + after_update :welcome_after_confirm, if: lambda { confirmation_token_changed? && confirmation_token.nil? } + after_create :send_welcome_email, if: lambda { email_is_known? } + scope :by_name, order('name') scope :visible, where(:visible => true) scope :confirmed, where('confirmed_at IS NOT NULL') @@ -299,13 +303,29 @@ class Enterprise < ActiveRecord::Base private - def email_check + def email_is_known? + owner.enterprises.confirmed.map(&:email).include?(email) + end + + def confirmation_check # Skip confirmation/reconfirmation if the new email has already been confirmed - if owner.enterprises.confirmed.map(&:email).include?(email) + if email_is_known? new_record? ? skip_confirmation! : skip_reconfirmation! end end + def welcome_after_confirm + # Send welcome email if we are confirming a newly created enterprise + # Note: this callback only runs on email confirmation + if confirmed? && unconfirmed_email.nil? && !unconfirmed_email_changed? + send_welcome_email + end + end + + def send_welcome_email + EnterpriseMailer.welcome(self).deliver + end + def strip_url(url) url.andand.sub(/(https?:\/\/)?/, '') end diff --git a/app/views/enterprise_mailer/confirmation_instructions.html.haml b/app/views/enterprise_mailer/confirmation_instructions.html.haml index 7d462a7ca2..bc2de61370 100644 --- a/app/views/enterprise_mailer/confirmation_instructions.html.haml +++ b/app/views/enterprise_mailer/confirmation_instructions.html.haml @@ -1,5 +1,5 @@ %h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} - = "Welcome, #{@resource.contact}!" + = "Hi, #{@resource.contact}!" %p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} = "Please confirm email address for your enterprise " %strong diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml new file mode 100644 index 0000000000..acd55b37fd --- /dev/null +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -0,0 +1,25 @@ +%h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} + = "Welcome, #{@enterprise.contact}!" +%p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} + %strong + = "#{@enterprise.name}" + = " is now on #{Spree::Config[:site_name]}." + +%p   + +/ Callout Panel +%p.callout{:style => "margin: 0; padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + = "Click the link below to update your profile or manage other details for " + %strong + = "#{@enterprise.name}." + %br + %strong + = link_to "Manage #{@enterprise.name} »", spree.admin_url + +%p   + +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + We're so excited that you're joining the Open Food Network! Don't hestitate to get in touch if you have any questions. +%p   + += render 'shared/mailers/social_and_contact' \ No newline at end of file diff --git a/spec/mailers/enterprise_mailer_spec.rb b/spec/mailers/enterprise_mailer_spec.rb index dd1e150eb7..5797fcbbb1 100644 --- a/spec/mailers/enterprise_mailer_spec.rb +++ b/spec/mailers/enterprise_mailer_spec.rb @@ -13,4 +13,11 @@ describe EnterpriseMailer do mail = ActionMailer::Base.deliveries.first expect(mail.subject).to eq "Please confirm your email for #{enterprise.name}" end + + it "should send a welcome email when given an enterprise" do + EnterpriseMailer.welcome(enterprise).deliver + ActionMailer::Base.deliveries.count.should == 1 + mail = ActionMailer::Base.deliveries.first + expect(mail.subject).to eq "#{enterprise.name} is now on #{Spree::Config[:site_name]}" + end end \ No newline at end of file diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index c477ccda68..50b8204a9c 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -8,16 +8,32 @@ describe Enterprise do let!(:user) { create_enterprise_user( enterprise_limit: 2 ) } let!(:enterprise) { create(:enterprise, owner: user) } - it "when the email address has not already been confirmed" do - mail_message = double "Mail::Message" - expect(EnterpriseMailer).to receive(:confirmation_instructions).and_return mail_message - mail_message.should_receive :deliver - create(:enterprise, owner: user, email: "unknown@email.com", confirmed_at: nil ) + context "when the email address has not already been confirmed" do + it "sends a confirmation email" do + mail_message = double "Mail::Message" + expect(EnterpriseMailer).to receive(:confirmation_instructions).and_return mail_message + mail_message.should_receive :deliver + create(:enterprise, owner: user, email: "unknown@email.com", confirmed_at: nil ) + end + + it "does not send a welcome email" do + expect(EnterpriseMailer).to_not receive(:welcome) + create(:enterprise, owner: user, email: "unknown@email.com", confirmed_at: nil ) + end end - it "when the email address has already been confirmed" do - expect(EnterpriseMailer).to_not receive(:confirmation_instructions) - e = create(:enterprise, owner: user, email: enterprise.email, confirmed_at: nil) + context "when the email address has already been confirmed" do + it "does not send a confirmation email" do + expect(EnterpriseMailer).to_not receive(:confirmation_instructions) + create(:enterprise, owner: user, email: enterprise.email, confirmed_at: nil) + end + + it "sends a welcome email" do + mail_message = double "Mail::Message" + expect(EnterpriseMailer).to receive(:welcome).and_return mail_message + mail_message.should_receive :deliver + create(:enterprise, owner: user, email: enterprise.email, confirmed_at: nil) + end end end @@ -38,6 +54,35 @@ describe Enterprise do enterprise.update_attributes!(email: "second.known.email@email.com") end end + + describe "on email confirmation" do + let!(:user) { create_enterprise_user( enterprise_limit: 2 ) } + let!(:unconfirmed_enterprise) { create(:enterprise, owner: user, confirmed_at: nil) } + + context "when we are confirming an email address for the first time for the enterprise" do + it "sends a welcome email" do + # unconfirmed_email is blank if we are not reconfirming an email + unconfirmed_enterprise.unconfirmed_email = nil + unconfirmed_enterprise.save! + + mail_message = double "Mail::Message" + expect(EnterpriseMailer).to receive(:welcome).and_return mail_message + mail_message.should_receive :deliver + unconfirmed_enterprise.confirm! + end + end + + context "when we are reconfirming the email address for the enterprise" do + it "does not send a welcome email" do + # unconfirmed_email is present if we are reconfirming an email + unconfirmed_enterprise.unconfirmed_email = "unconfirmed@email.com" + unconfirmed_enterprise.save! + + expect(EnterpriseMailer).to_not receive(:welcome) + unconfirmed_enterprise.confirm! + end + end + end end describe "associations" do From 6b21bbdf746f3d8d2f460d863207c01463e39745 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 12 Nov 2014 12:30:32 +1100 Subject: [PATCH 201/681] Reconfirmation email sends to the right address --- app/mailers/enterprise_mailer.rb | 2 +- spec/mailers/enterprise_mailer_spec.rb | 28 +++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/mailers/enterprise_mailer.rb b/app/mailers/enterprise_mailer.rb index 2e6e83d92c..7504597589 100644 --- a/app/mailers/enterprise_mailer.rb +++ b/app/mailers/enterprise_mailer.rb @@ -15,7 +15,7 @@ class EnterpriseMailer < Spree::BaseMailer find_enterprise(record) opts = { subject: "Please confirm your email for #{@enterprise.name}", - to: @enterprise.email, + to: ( @enterprise.unconfirmed_email || @enterprise.email ), from: from_address, } devise_mail(record, :confirmation_instructions, opts) diff --git a/spec/mailers/enterprise_mailer_spec.rb b/spec/mailers/enterprise_mailer_spec.rb index 5797fcbbb1..65480ccdad 100644 --- a/spec/mailers/enterprise_mailer_spec.rb +++ b/spec/mailers/enterprise_mailer_spec.rb @@ -7,11 +7,29 @@ describe EnterpriseMailer do ActionMailer::Base.deliveries = [] end - it "should send an email confirmation when given an enterprise" do - EnterpriseMailer.confirmation_instructions(enterprise, 'token').deliver - ActionMailer::Base.deliveries.count.should == 1 - mail = ActionMailer::Base.deliveries.first - expect(mail.subject).to eq "Please confirm your email for #{enterprise.name}" + context "when given an enterprise without an unconfirmed_email" do + it "should send an email confirmation to email" do + EnterpriseMailer.confirmation_instructions(enterprise, 'token').deliver + ActionMailer::Base.deliveries.count.should == 1 + mail = ActionMailer::Base.deliveries.first + expect(mail.subject).to eq "Please confirm your email for #{enterprise.name}" + expect(mail.to).to include enterprise.email + end + end + + context "when given an enterprise with an unconfirmed_email" do + before do + enterprise.unconfirmed_email = "unconfirmed@email.com" + enterprise.save! + end + + it "should send an email confirmation to unconfirmed_email" do + EnterpriseMailer.confirmation_instructions(enterprise, 'token').deliver + ActionMailer::Base.deliveries.count.should == 1 + mail = ActionMailer::Base.deliveries.first + expect(mail.subject).to eq "Please confirm your email for #{enterprise.name}" + expect(mail.to).to include enterprise.unconfirmed_email + end end it "should send a welcome email when given an enterprise" do From 2c67066366273bd57917051077c7e873963dc9c6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 12 Nov 2014 12:31:01 +1100 Subject: [PATCH 202/681] Add alert to enterprise edit page to remind user that confirmation email has been sent --- app/views/admin/enterprises/_form.html.haml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 20bd174de9..4dfe15ec11 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -120,6 +120,13 @@ = f.label :contact, 'Name' .omega.eight.columns = f.text_field :contact, { placeholder: "eg. Gustav Plum"} + -if @enterprise.unconfirmed_email + .alert-box + Email change is pending. + We've sent a confirmation email to + %strong= "#{@enterprise.unconfirmed_email}." + = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.unconfirmed_email } ), method: :post) + %a.close{ href: "#" } × .row .alpha.three.columns = f.label :email @@ -184,7 +191,7 @@ -# ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], -# ['html', 'insertImage', 'insertLink', 'insertVideo'] %text-angular{'ng-model' => 'htmlVariable', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', - 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", + 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} %fieldset.eleven.columns.alpha.no-border-bottom %legend IMAGES From 7a719952fc8fef7701275c77186d4ac104ae705a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 12 Nov 2014 15:19:59 +1100 Subject: [PATCH 203/681] Adding informative messages about email confirmations to enterprise form --- .../enterprise_controller.js.coffee | 1 + .../api/admin/enterprise_serializer.rb | 2 +- app/views/admin/enterprises/_form.html.haml | 389 +++++++++--------- .../admin/enterprises/_form_data.html.haml | 4 + .../admin/enterprises/_ng_form.html.haml | 22 +- app/views/admin/enterprises/edit.html.haml | 10 +- app/views/admin/enterprises/new.html.haml | 10 +- 7 files changed, 224 insertions(+), 214 deletions(-) create mode 100644 app/views/admin/enterprises/_form_data.html.haml diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index cfcf6319af..e9a1bc077f 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -6,6 +6,7 @@ angular.module("admin.enterprises") $scope.navClear = NavigationCheck.clear # htmlVariable is used by textAngular wysiwyg for the long descrtiption. $scope.htmlVariable = longDescription + $scope.pristineEmail = $scope.Enterprise.email # Provide a callback for generating warning messages displayed before leaving the page. This is passed in # from a directive "nav-check" in the page - if we pass it here it will be called in the test suite, diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index a23764b84a..37c7e9af27 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -1,4 +1,4 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids - attributes :producer_profile_only + attributes :producer_profile_only, :email end diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 4dfe15ec11..0d0a54e983 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -1,216 +1,217 @@ -- content_for :page_actions do - %li= button_link_to "Back to enterprises list", main_app.admin_enterprises_path, icon: 'icon-arrow-left' +%fieldset.eleven.columns.alpha.no-border-bottom + %legend Primary Details + .row + .alpha.eleven.columns + .three.columns.alpha + = f.label :name + .eight.columns.omega + = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles" } + .row + .alpha.eleven.columns + .three.columns.alpha + = f.label :group_ids, 'Groups' + .with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."} + %a What's this? + .eight.columns.omega + = f.collection_select :group_ids, EnterpriseGroup.all, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..." + - if spree_current_user.admin? + .row + .three.columns.alpha + =f.label :owner_id, 'Owner' + .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} + %a What's this? + .eight.columns + - owner_email = @enterprise.andand.owner.andand.email || "" + = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email -.fullwidth_inputs - %fieldset.eleven.columns.alpha.no-border-bottom - %legend Primary Details + .row + .three.columns.alpha + %label Primary Producer + .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %a What's this? + .five.columns.omega + = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' +   + = f.label :is_primary_producer, 'Producer' + - if spree_current_user.admin? .row .alpha.eleven.columns .three.columns.alpha - = f.label :name - .eight.columns.omega - = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles" } - .row - .alpha.eleven.columns - .three.columns.alpha - = f.label :group_ids, 'Groups' - .with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."} + = f.label :sells, 'Sells' + .with-tip{'data-powertip' => "None - enterprise does not sell to customers directly.
    Own - Enterprise sells own products to customers.
    Any - Enterprise can sell own or other enterprises products.
    "} %a What's this? - - .eight.columns.omega - = f.collection_select :group_ids, EnterpriseGroup.all, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..." - - if spree_current_user.admin? - .row - .three.columns.alpha - =f.label :owner_id, 'Owner' - .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} - %a What's this? - .eight.columns - - owner_email = @enterprise.andand.owner.andand.email || "" - = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email - + .two.columns + = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "None", value: "none" + .two.columns + = f.radio_button :sells, "own", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "Own", value: "own" + .four.columns.omega + = f.radio_button :sells, "any", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "Any", value: "any" + .row + .three.columns.alpha + %label Visible in search? + .with-tip{'data-powertip' => "Determines whether this enterprise will be visible to customers when searching the site."} + %a What's this? + .two.columns + = f.radio_button :visible, true +   + = f.label :visible, "Visible", :value => "true" + .five.columns.omega + = f.radio_button :visible, false +   + = f.label :visible, "Not Visible", :value => "false" + - if @enterprise.is_distributor + - # TODO: Angularise this .row .three.columns.alpha - %label Primary Producer - .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %label Link to shop front + .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} %a What's this? - .five.columns.omega - = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' -   - = f.label :is_primary_producer, 'Producer' - - if spree_current_user.admin? - .row - .alpha.eleven.columns - .three.columns.alpha - = f.label :sells, 'Sells' - .with-tip{'data-powertip' => "None - enterprise does not sell to customers directly.
    Own - Enterprise sells own products to customers.
    Any - Enterprise can sell own or other enterprises products.
    "} - %a What's this? - .two.columns - = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' -   - = f.label :sells, "None", value: "none" - .two.columns - = f.radio_button :sells, "own", 'ng-model' => 'Enterprise.sells' -   - = f.label :sells, "Own", value: "own" - .four.columns.omega - = f.radio_button :sells, "any", 'ng-model' => 'Enterprise.sells' -   - = f.label :sells, "Any", value: "any" + .eight.columns.omega + = main_app.shop_enterprise_url(@enterprise) + + += f.fields_for :address do |af| + %fieldset.eleven.columns.alpha.no-border-bottom + %legend Address .row .three.columns.alpha - %label Visible in search? - .with-tip{'data-powertip' => "Determines whether this enterprise will be visible to customers when searching the site."} - %a What's this? - .two.columns - = f.radio_button :visible, true -   - = f.label :visible, "Visible", :value => "true" - .five.columns.omega - = f.radio_button :visible, false -   - = f.label :visible, "Not Visible", :value => "false" - - if @enterprise.is_distributor - - # TODO: Angularise this - .row - .three.columns.alpha - %label Link to shop front - .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} - %a What's this? - .eight.columns.omega - = main_app.shop_enterprise_url(@enterprise) - - - = f.fields_for :address do |af| - %fieldset.eleven.columns.alpha.no-border-bottom - %legend Address - .row - .three.columns.alpha - = af.label :address1 - .eight.columns.omega - = af.text_field :address1, { placeholder: "eg. 123 High Street"} - .row - .alpha.three.columns - = af.label :address2 - .eight.columns.omega - = af.text_field :address2 - .row - .three.columns.alpha - = af.label :city, 'Suburb' - \/ - = af.label :zipcode, 'Postcode' - .four.columns - = af.text_field :city, { placeholder: "eg. Northcote"} - .four.columns.omega - = af.text_field :zipcode, { placeholder: "eg. 3070"} - .row - .three.columns.alpha - = af.label :state_id, 'State' - \/ - = af.label :country_id, 'Country' - .four.columns - = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" - .four.columns.omega - = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" - %fieldset.eleven.columns.alpha.no-border-bottom - %legend Contact Details - .row - .alpha.three.columns - = f.label :contact, 'Name' - .omega.eight.columns - = f.text_field :contact, { placeholder: "eg. Gustav Plum"} - -if @enterprise.unconfirmed_email - .alert-box - Email change is pending. - We've sent a confirmation email to - %strong= "#{@enterprise.unconfirmed_email}." - = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.unconfirmed_email } ), method: :post) - %a.close{ href: "#" } × - .row - .alpha.three.columns - = f.label :email - .omega.eight.columns - = f.text_field :email, { placeholder: "eg. gustav@truffles.com"} - .row - .alpha.three.columns - = f.label :phone - .omega.eight.columns - = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} - %fieldset.eleven.columns.alpha.no-border-bottom - %legend Enterprise Details - .row - .alpha.three.columns - = f.label :abn, 'ABN' - .omega.eight.columns - = f.text_field :abn, { placeholder: "eg. 99 123 456 789"} - .row - .alpha.three.columns - = f.label :acn, 'ACN' - .omega.eight.columns - = f.text_field :acn, { placeholder: "eg. 123 456 789"} - .row - .alpha.three.columns - = f.label :website - .omega.eight.columns - = f.text_field :website, { placeholder: "eg. www.truffles.com"} - .row - .alpha.three.columns - = f.label :facebook, 'Facebook' - .omega.eight.columns - = f.text_field :facebook - .row - .alpha.three.columns - = f.label :instagram, 'Instagram' - .omega.eight.columns - = f.text_field :instagram - .row - .alpha.three.columns - = f.label :linkedin, 'LinkedIn' - .omega.eight.columns - = f.text_field :linkedin - .row - .alpha.three.columns - = f.label :twitter - .omega.eight.columns - = f.text_field :twitter, { placeholder: "eg. @the_prof" } + = af.label :address1 + .eight.columns.omega + = af.text_field :address1, { placeholder: "eg. 123 High Street"} + .row + .alpha.three.columns + = af.label :address2 + .eight.columns.omega + = af.text_field :address2 + .row + .three.columns.alpha + = af.label :city, 'Suburb' + \/ + = af.label :zipcode, 'Postcode' + .four.columns + = af.text_field :city, { placeholder: "eg. Northcote"} + .four.columns.omega + = af.text_field :zipcode, { placeholder: "eg. 3070"} + .row + .three.columns.alpha + = af.label :state_id, 'State' + \/ + = af.label :country_id, 'Country' + .four.columns + = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" + .four.columns.omega + = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" %fieldset.eleven.columns.alpha.no-border-bottom - %legend About Us + %legend Contact Details .row .alpha.three.columns - = f.label :description, 'Short Description' + = f.label :contact, 'Name' .omega.eight.columns - = f.text_field :description, maxlength: 255, placeholder: 'Tell us about your enterprise in one or two sentences' + = f.text_field :contact, { placeholder: "eg. Gustav Plum"} + -if @enterprise.unconfirmed_email + .alert-box + Email change is pending. + We've sent a confirmation email to + %strong= "#{@enterprise.unconfirmed_email}." + = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.unconfirmed_email } ), method: :post) + %a.close{ href: "#" } × .row .alpha.three.columns - = f.label :long_description, 'About Us' + = f.label :email .omega.eight.columns - -# textAngular toolbar options, add to the ta-toolbar array below and separate into groups with extra ],[ if needed: - -# ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'], - -# ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'], - -# ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], - -# ['html', 'insertImage', 'insertLink', 'insertVideo'] - %text-angular{'ng-model' => 'htmlVariable', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', - 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", - 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} - %fieldset.eleven.columns.alpha.no-border-bottom - %legend IMAGES - .row + = f.text_field :email, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email" } + .row{ ng: { hide: "pristineEmail == null || pristineEmail == Enterprise.email"} } .alpha.three.columns - = f.label :logo - %br - 100 x 100 pixels +   .omega.eight.columns - = image_tag @object.logo(:medium) 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 in "About Us"' - %br/ - %span{ style: 'font-weight:bold' } PLEASE NOTE: - Any promo image uploaded here will be cropped to 1200 x 260. - The promo image is displayed at the top of an enterprise's profile page and pop-ups. + Note: A new email address may need to be confirmed prior to use + .row + .alpha.three.columns + = f.label :phone .omega.eight.columns - = image_tag @object.promo_image(:large) if @object.promo_image.present? - = f.file_field :promo_image + = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} + %fieldset.eleven.columns.alpha.no-border-bottom + %legend Enterprise Details + .row + .alpha.three.columns + = f.label :abn, 'ABN' + .omega.eight.columns + = f.text_field :abn, { placeholder: "eg. 99 123 456 789"} + .row + .alpha.three.columns + = f.label :acn, 'ACN' + .omega.eight.columns + = f.text_field :acn, { placeholder: "eg. 123 456 789"} + .row + .alpha.three.columns + = f.label :website + .omega.eight.columns + = f.text_field :website, { placeholder: "eg. www.truffles.com"} + .row + .alpha.three.columns + = f.label :facebook, 'Facebook' + .omega.eight.columns + = f.text_field :facebook + .row + .alpha.three.columns + = f.label :instagram, 'Instagram' + .omega.eight.columns + = f.text_field :instagram + .row + .alpha.three.columns + = f.label :linkedin, 'LinkedIn' + .omega.eight.columns + = f.text_field :linkedin + .row + .alpha.three.columns + = f.label :twitter + .omega.eight.columns + = f.text_field :twitter, { placeholder: "eg. @the_prof" } +%fieldset.eleven.columns.alpha.no-border-bottom + %legend About Us + .row + .alpha.three.columns + = f.label :description, 'Short Description' + .omega.eight.columns + = f.text_field :description, maxlength: 255, placeholder: 'Tell us about your enterprise in one or two sentences' + .row + .alpha.three.columns + = f.label :long_description, 'About Us' + .omega.eight.columns + -# textAngular toolbar options, add to the ta-toolbar array below and separate into groups with extra ],[ if needed: + -# ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'], + -# ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'], + -# ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], + -# ['html', 'insertImage', 'insertLink', 'insertVideo'] + %text-angular{'ng-model' => 'htmlVariable', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', + 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", + 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} +%fieldset.eleven.columns.alpha.no-border-bottom + %legend IMAGES + .row + .alpha.three.columns + = f.label :logo + %br + 100 x 100 pixels + .omega.eight.columns + = image_tag @object.logo(:medium) 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 in "About Us"' + %br/ + %span{ style: 'font-weight:bold' } PLEASE NOTE: + Any promo image uploaded here will be cropped to 1200 x 260. + The promo image is displayed at the top of an enterprise's profile page and pop-ups. + + .omega.eight.columns + = image_tag @object.promo_image(:large) if @object.promo_image.present? + = f.file_field :promo_image diff --git a/app/views/admin/enterprises/_form_data.html.haml b/app/views/admin/enterprises/_form_data.html.haml new file mode 100644 index 0000000000..a7d34a1aad --- /dev/null +++ b/app/views/admin/enterprises/_form_data.html.haml @@ -0,0 +1,4 @@ += admin_inject_enterprise += admin_inject_enterprise_long_description += admin_inject_payment_methods += admin_inject_shipping_methods \ No newline at end of file diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index e3ebe95877..e5783f0d8c 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -1,11 +1,11 @@ -= admin_inject_enterprise -= admin_inject_enterprise_long_description -= admin_inject_payment_methods -= admin_inject_shipping_methods - -.sixteen.columns.alpha{ ng: { app: 'admin.enterprises', controller: 'enterpriseCtrl' }, nav: { check: '', callback: 'enterpriseNavCallback()' }} - .eleven.columns.alpha - = render 'form', f: f - .one.column   - .four.columns.omega - = render 'sidebar', f: f += form_for [main_app, :admin, @enterprise], html: { name: "enterprise", "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl' , "nav-check" => '', "nav-callback" => 'enterpriseNavCallback()' } do |f| + .row + .sixteen.columns.alpha + .eleven.columns.alpha.fullwidth_inputs + = render 'form', f: f + .one.column   + .four.columns.omega + = render 'sidebar', f: f + .row + .twelve.columns.alpha + = render partial: "spree/admin/shared/#{action}_resource_links" \ No newline at end of file diff --git a/app/views/admin/enterprises/edit.html.haml b/app/views/admin/enterprises/edit.html.haml index 3c132bf5e2..19ae0f3b44 100644 --- a/app/views/admin/enterprises/edit.html.haml +++ b/app/views/admin/enterprises/edit.html.haml @@ -4,7 +4,9 @@ Editing: = @enterprise.name -= form_for [main_app, :admin, @enterprise], html: { "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl' , "nav-check" => '', "nav-callback" => 'enterpriseNavCallback()' } do |f| - = render 'ng_form', f: f - .twelve.columns.alpha - = render partial: 'spree/admin/shared/edit_resource_links' +- content_for :page_actions do + %li= button_link_to "Back to enterprises list", main_app.admin_enterprises_path, icon: 'icon-arrow-left' + += render 'admin/enterprises/form_data' + += render 'admin/enterprises/ng_form', action: 'edit' diff --git a/app/views/admin/enterprises/new.html.haml b/app/views/admin/enterprises/new.html.haml index 3df3551f47..634044bf30 100644 --- a/app/views/admin/enterprises/new.html.haml +++ b/app/views/admin/enterprises/new.html.haml @@ -3,7 +3,9 @@ - content_for :page_title do New Enterprise -= form_for [main_app, :admin, @enterprise], html: { "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl' , "nav-check" => '', "nav-callback" => 'enterpriseNavCallback()' } do |f| - = render 'ng_form', f: f - .twelve.columns.alpha - = render partial: 'spree/admin/shared/new_resource_links' +- content_for :page_actions do + %li= button_link_to "Back to enterprises list", main_app.admin_enterprises_path, icon: 'icon-arrow-left' + += render 'admin/enterprises/form_data' + += render 'admin/enterprises/ng_form', action: 'new' From 7282c7174f5cb12cf4900e36d4f38b3f38219eba Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 12 Nov 2014 15:55:48 +1100 Subject: [PATCH 204/681] Moving alert box to top of section --- app/views/admin/enterprises/_form.html.haml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 0d0a54e983..ecdd52f06f 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -110,11 +110,6 @@ = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" %fieldset.eleven.columns.alpha.no-border-bottom %legend Contact Details - .row - .alpha.three.columns - = f.label :contact, 'Name' - .omega.eight.columns - = f.text_field :contact, { placeholder: "eg. Gustav Plum"} -if @enterprise.unconfirmed_email .alert-box Email change is pending. @@ -122,6 +117,11 @@ %strong= "#{@enterprise.unconfirmed_email}." = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.unconfirmed_email } ), method: :post) %a.close{ href: "#" } × + .row + .alpha.three.columns + = f.label :contact, 'Name' + .omega.eight.columns + = f.text_field :contact, { placeholder: "eg. Gustav Plum"} .row .alpha.three.columns = f.label :email From 4e655d786691da490acfe2c0c660aa79f1f90b5b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 12 Nov 2014 14:39:59 +1100 Subject: [PATCH 205/681] Uncouple migrations from models --- ...30919010513_ensure_shipping_methods_have_distributors.rb | 4 ++++ db/migrate/20140402033428_add_foreign_keys.rb | 6 +++++- db/migrate/20140828023619_add_owner_to_enterprise.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/db/migrate/20130919010513_ensure_shipping_methods_have_distributors.rb b/db/migrate/20130919010513_ensure_shipping_methods_have_distributors.rb index 271fd5f9df..e233227242 100644 --- a/db/migrate/20130919010513_ensure_shipping_methods_have_distributors.rb +++ b/db/migrate/20130919010513_ensure_shipping_methods_have_distributors.rb @@ -1,4 +1,8 @@ class EnsureShippingMethodsHaveDistributors < ActiveRecord::Migration + class Enterprise < ActiveRecord::Base + scope :is_distributor, where(is_distributor: true) + end + def up d = Enterprise.is_distributor.first sms = Spree::ShippingMethod.where('distributor_id IS NULL') diff --git a/db/migrate/20140402033428_add_foreign_keys.rb b/db/migrate/20140402033428_add_foreign_keys.rb index fce1ffd009..1535f4f3f9 100644 --- a/db/migrate/20140402033428_add_foreign_keys.rb +++ b/db/migrate/20140402033428_add_foreign_keys.rb @@ -1,12 +1,15 @@ class AddForeignKeys < ActiveRecord::Migration class AdjustmentMetadata < ActiveRecord::Base; end + class CoordinatorFee < ActiveRecord::Base; end + class Enterprise < ActiveRecord::Base + belongs_to :address, :class_name => 'Spree::Address' + end class ExchangeVariant < ActiveRecord::Base; end class Spree::InventoryUnit < ActiveRecord::Base; end class Spree::LineItem < ActiveRecord::Base; end class Spree::Address < ActiveRecord::Base; end class Spree::Order < ActiveRecord::Base; end class Spree::Taxon < ActiveRecord::Base; end - class CoordinatorFee < ActiveRecord::Base; end def change setup_foreign_keys @@ -60,6 +63,7 @@ class AddForeignKeys < ActiveRecord::Migration address = Spree::Address.create!(firstname: 'Dummy distributor', lastname: 'Dummy distributor', phone: '12345678', state: state, address1: 'Dummy distributor', city: 'Dummy distributor', zipcode: '1234', country: country) + Enterprise.reset_column_information deleted_distributor = Enterprise.create!(name: "Deleted distributor", address: address) orphaned_orders = Spree::Order.joins('LEFT OUTER JOIN enterprises ON enterprises.id=spree_orders.distributor_id').where('enterprises.id IS NULL') diff --git a/db/migrate/20140828023619_add_owner_to_enterprise.rb b/db/migrate/20140828023619_add_owner_to_enterprise.rb index 8491e9c599..d59cd2250f 100644 --- a/db/migrate/20140828023619_add_owner_to_enterprise.rb +++ b/db/migrate/20140828023619_add_owner_to_enterprise.rb @@ -7,7 +7,7 @@ class AddOwnerToEnterprise < ActiveRecord::Migration owner = e.users.find{ |u| !u.admin? } admin_owner = e.users.find &:admin? any_admin = Spree::User.admin.first - any_user = Spree::User.first + any_user = Spree::User.first || Spree::User.new(email: 'owner@example.com', password: 'owner123').tap { |u| u.save(validate: false) } e.update_column :owner_id, (owner || admin_owner || any_admin || any_user ) end From 2ca2d5327377419005d58bf543eb6455307f66c4 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 12 Nov 2014 16:17:16 +1100 Subject: [PATCH 206/681] Do not include perftools gem by default - it breaks Travis --- Gemfile | 4 +++- Gemfile.lock | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 2e03d65a35..4fec11abeb 100644 --- a/Gemfile +++ b/Gemfile @@ -91,7 +91,9 @@ end group :test do gem 'webmock' - gem 'perftools.rb' + + # See spec/spec_helper.rb for instructions + #gem 'perftools.rb' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index b98ceb4789..4af4129c5b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -356,7 +356,6 @@ GEM xml-simple paypal-sdk-merchant (1.106.1) paypal-sdk-core (~> 0.2.3) - perftools.rb (2.0.1) pg (0.13.2) poltergeist (1.5.0) capybara (~> 2.1) @@ -550,7 +549,6 @@ DEPENDENCIES newrelic_rpm oj paperclip - perftools.rb pg poltergeist pry-debugger From 43ddac84b0fb29137b4dcccc1d8144f873ae3c57 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 12 Nov 2014 16:25:22 +1100 Subject: [PATCH 207/681] I like nice code --- db/migrate/20140828023619_add_owner_to_enterprise.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/migrate/20140828023619_add_owner_to_enterprise.rb b/db/migrate/20140828023619_add_owner_to_enterprise.rb index d59cd2250f..5b25d4417c 100644 --- a/db/migrate/20140828023619_add_owner_to_enterprise.rb +++ b/db/migrate/20140828023619_add_owner_to_enterprise.rb @@ -7,7 +7,8 @@ class AddOwnerToEnterprise < ActiveRecord::Migration owner = e.users.find{ |u| !u.admin? } admin_owner = e.users.find &:admin? any_admin = Spree::User.admin.first - any_user = Spree::User.first || Spree::User.new(email: 'owner@example.com', password: 'owner123').tap { |u| u.save(validate: false) } + any_user = Spree::User.first + any_user ||= Spree::User.new(email: 'owner@example.com', password: 'owner123').tap { |u| u.save(validate: false) } e.update_column :owner_id, (owner || admin_owner || any_admin || any_user ) end From 932d571d2ce2e763a932df5f72e5f93ad21e9eae Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Wed, 12 Nov 2014 11:47:26 +0000 Subject: [PATCH 208/681] 266: Updating to incorporate Rohans suggestions. Searching on payment method name rather than id --- app/helpers/spree/reports_helper.rb | 12 +++++------- app/models/spree/order_decorator.rb | 6 ++++++ .../admin/reports/order_cycle_management.html.haml | 4 ++-- .../order_cycle_management_report.rb | 5 ++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index 0378e7db46..5854ede743 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -13,14 +13,12 @@ module Spree #I don't like that this is done in two loops, but redundant list entries # were created otherwise... def report_payment_method_options(orders) - payment_method_list = {} - orders.map do |o| - payment_method_name = o.payments.first.payment_method.andand.name - payment_method_id = o.payments.first.payment_method.andand.id - payment_method_list[payment_method_name] = payment_method_id + payment_method_list = [] + orders.map do |o| + payment_method_list << o.payments.first.payment_method.andand.name end - payment_method_list.each do |key, value| - [ "#{value}".html_safe, key] + payment_method_list.uniq.each do |pm| + [ "#{pm}".html_safe, pm] end end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index fcc6054f3b..228410a116 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -66,6 +66,12 @@ Spree::Order.class_eval do where("state != ?", state) } + scope :with_payment_method_name, lambda { |payment_method_name| + joins(:payments => :payment_method). + where('spree_payment_methods.name = ?', payment_method_name). + select('DISTINCT spree_orders.*') + } + # -- Methods def products_available_from_new_distribution diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml index a88e55ddde..a40281e255 100644 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -9,8 +9,8 @@ = label_tag nil, "Payment Methods (hold Ctrl to select multiple payment methods)" %br - = select_tag(:payment_method_id, - options_for_select(report_payment_method_options(@orders), params[:payment_method_id]), + = select_tag(:payment_method_name, + options_for_select(report_payment_method_options(@orders), params[:payment_method_name]), multiple: true, include_blank: true) %br %br diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index 70c5de8f29..3208ab2d33 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -43,9 +43,8 @@ module OpenFoodNetwork end def filter_to_payment_method (orders) - if params.has_key? (:payment_method_id) - orders.joins(:payments) - .where(:spree_payments => {:payment_method_id => params[:payment_method_id]} ) + if params.has_key? (:payment_method_name) + orders.with_payment_method_name(params[:payment_method_name]) else orders end From 15f29f4c8ecd661453b5091f0eab3f116fc1ed24 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Wed, 12 Nov 2014 18:52:25 +0000 Subject: [PATCH 209/681] 266: Adding ability to search by distribution --- app/helpers/spree/reports_helper.rb | 10 +++++++++- .../admin/reports/order_cycle_management.html.haml | 7 ++++++- .../order_cycle_management_report.rb | 13 +++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index 5854ede743..967cd7fd36 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -22,7 +22,15 @@ module Spree end end - + def report_distribution_options(orders) + distribution_list = [] + orders.map do |o| + distribution_list << o.shipping_method.andand.name + end + distribution_list.uniq.each do |pm| + [ "#{pm}".html_safe, pm] + end + end end end diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml index a40281e255..3428cf3977 100644 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -11,7 +11,12 @@ = select_tag(:payment_method_name, options_for_select(report_payment_method_options(@orders), params[:payment_method_name]), - multiple: true, include_blank: true) + multiple: true, include_blank: true) + %br + %br + = select_tag(:distribution_name, + options_for_select(report_distribution_options(@orders), params[:distribution_name]), + include_blank: true) %br %br = check_box_tag :csv diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index 3208ab2d33..fb4da5468d 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -7,7 +7,7 @@ module OpenFoodNetwork end def header - ["First Name", "Last Name", "Email", "Phone", "Hub", "Payment Method", "Amount ", "Amount Paid"] + ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount ", "Amount Paid"] end @@ -20,6 +20,7 @@ module OpenFoodNetwork order.email, ba.phone, order.distributor.andand.name, + order.shipping_method.andand.name, order.payments.first.andand.payment_method.andand.name, order.payments.first.amount ] @@ -31,7 +32,7 @@ module OpenFoodNetwork end def filter(orders) - filter_for_user filter_active filter_to_order_cycle filter_to_payment_method orders + filter_for_user filter_active filter_to_order_cycle filter_to_payment_method filter_to_distribution orders end def filter_for_user (orders) @@ -50,6 +51,14 @@ module OpenFoodNetwork end end + def filter_to_distribution (orders) + if params.has_key? (:distribution_name) + orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:distribution_name]) + else + orders + end + end + def filter_to_order_cycle(orders) if params[:order_cycle_id].to_i > 0 orders.where(order_cycle_id: params[:order_cycle_id]) From 43bac4079f2f54c94e7a3ed2ff9e3ea521269b66 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 13 Nov 2014 15:21:02 +1100 Subject: [PATCH 210/681] Show hubs that are not ready for checkout so that we can view SEFH - Sample :/ --- app/controllers/base_controller.rb | 2 +- spec/controllers/base_controller_spec.rb | 2 +- spec/controllers/home_controller_spec.rb | 3 +-- spec/controllers/map_controller_spec.rb | 3 +-- spec/controllers/producers_controller_spec.rb | 3 +-- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index 88c0f89aec..2ac35fa3ad 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -13,6 +13,6 @@ class BaseController < ApplicationController before_filter :check_order_cycle_expiry def load_active_distributors - @active_distributors ||= Enterprise.distributors_with_active_order_cycles.ready_for_checkout + @active_distributors ||= Enterprise.distributors_with_active_order_cycles end end diff --git a/spec/controllers/base_controller_spec.rb b/spec/controllers/base_controller_spec.rb index 822da57ba5..1040b0594c 100644 --- a/spec/controllers/base_controller_spec.rb +++ b/spec/controllers/base_controller_spec.rb @@ -26,7 +26,7 @@ describe BaseController do end it "loads active_distributors" do - Enterprise.stub_chain(:distributors_with_active_order_cycles, :ready_for_checkout) { 'active distributors' } + Enterprise.stub(:distributors_with_active_order_cycles) { 'active distributors' } controller.load_active_distributors.should == 'active distributors' end end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index 16367eb812..bbbff9a7b1 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -6,8 +6,7 @@ describe HomeController do let!(:invisible_distributor) { create(:distributor_enterprise, visible: false) } before do - Enterprise.stub_chain(:distributors_with_active_order_cycles, :ready_for_checkout). - and_return [distributor] + Enterprise.stub(:distributors_with_active_order_cycles) { [distributor] } end it "sets active distributors" do diff --git a/spec/controllers/map_controller_spec.rb b/spec/controllers/map_controller_spec.rb index c4e98def8a..fab9b7ac22 100644 --- a/spec/controllers/map_controller_spec.rb +++ b/spec/controllers/map_controller_spec.rb @@ -4,8 +4,7 @@ describe MapController do it "loads active distributors" do active_distributors = double(:distributors) - Enterprise.stub_chain(:distributors_with_active_order_cycles, :ready_for_checkout). - and_return(active_distributors) + Enterprise.stub(:distributors_with_active_order_cycles) { active_distributors } get :index diff --git a/spec/controllers/producers_controller_spec.rb b/spec/controllers/producers_controller_spec.rb index 2b7377a471..ec3c39036c 100644 --- a/spec/controllers/producers_controller_spec.rb +++ b/spec/controllers/producers_controller_spec.rb @@ -4,8 +4,7 @@ describe ProducersController do let!(:distributor) { create(:distributor_enterprise) } before do - Enterprise.stub_chain(:distributors_with_active_order_cycles, :ready_for_checkout). - and_return([distributor]) + Enterprise.stub(:distributors_with_active_order_cycles) { [distributor] } Enterprise.stub(:all).and_return([distributor]) end From cc3959467d0de38266b6b847ca4f7e01aedc8e9e Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Thu, 13 Nov 2014 10:30:00 +0000 Subject: [PATCH 211/681] 285: Report downlaod naming - Updating to the cleaner suggestions of Rohan. Note to self, don't be lazy :-) --- .../spree/admin/reports_controller_decorator.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 8dfd5e0708..ee4bff6939 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -50,7 +50,7 @@ Spree::Admin::ReportsController.class_eval do @report_type = params[:report_type] @report = OpenFoodNetwork::CustomersReport.new spree_current_user, params - render_report(@report.header, @report.table, params[:csv], "customers_"+Time.now.strftime("%Y%m%d")+".csv") + render_report(@report.header, @report.table, params[:csv], "customers_#{timestamp}.csv") end def orders_and_distributors @@ -78,7 +78,7 @@ Spree::Admin::ReportsController.class_eval do csv << @report.header @report.table.each { |row| csv << row } end - send_data csv_string, :filename => "orders_and_distributors_"+Time.now.strftime("%Y%m%d")+".csv" + send_data csv_string, :filename => "orders_and_distributors_#{timestamp}.csv" end end @@ -230,7 +230,7 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(@line_items) - csv_file_name = "bulk_coop_"+Time.now.strftime("%Y%m%d")+".csv" + csv_file_name = "bulk_coop_#{timestamp}.csv" render_report(@header, @table, params[:csv], csv_file_name) end @@ -334,7 +334,7 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(table_items) - csv_file_name = "payments_"+Time.now.strftime("%Y%m%d")+".csv" + csv_file_name = "payments_#{timestamp}.csv" render_report(@header, @table, params[:csv], csv_file_name) @@ -564,7 +564,7 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(table_items) - csv_file_name = params[:report_type]+"_"+Time.now.strftime("%Y%m%d")+".csv" + csv_file_name = "#{params[:report_type]}_#{timestamp}.csv" render_report(@header, @table, params[:csv], csv_file_name) @@ -576,7 +576,7 @@ Spree::Admin::ReportsController.class_eval do @report = OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params #@table = @report.table #@header = @report.header - render_report(@report.header, @report.table, params[:csv], "products_and_inventory_"+Time.now.strftime("%Y%m%d")+".csv") + render_report(@report.header, @report.table, params[:csv], "products_and_inventory_#{timestamp}.csv") end def render_report (header, table, create_csv, csv_file_name) @@ -627,4 +627,8 @@ Spree::Admin::ReportsController.class_eval do end total_units.round(3) end + + def timestamp + Time.now.strftime("%Y%m%d") + end end From 65a5960fcc7afa3e6eae34834c700bafb425a792 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 13 Nov 2014 12:44:33 +1100 Subject: [PATCH 212/681] WIP: Add route for User and Enterprise report --- app/controllers/spree/admin/reports_controller_decorator.rb | 3 ++- config/routes.rb | 1 + spec/features/admin/reports_spec.rb | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 22dd41549f..038e12020a 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -613,7 +613,8 @@ Spree::Admin::ReportsController.class_eval do :orders_and_fulfillment => {:name => "Orders & Fulfillment Reports", :description => ''}, :customers => {:name => "Customers", :description => 'Customer details'}, :products_and_inventory => {:name => "Products & Inventory", :description => ''}, - :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" } + :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" }, + :users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" } } # Return only reports the user is authorized to view. reports.select { |action| can? action, :report } diff --git a/config/routes.rb b/config/routes.rb index 6dfe7cc900..a860ec8acf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -116,6 +116,7 @@ Spree::Core::Engine.routes.prepend do match '/admin/reports/bulk_coop' => 'admin/reports#bulk_coop', :as => "bulk_coop_admin_reports", :via => [:get, :post] match '/admin/reports/payments' => 'admin/reports#payments', :as => "payments_admin_reports", :via => [:get, :post] match '/admin/reports/orders_and_fulfillment' => 'admin/reports#orders_and_fulfillment', :as => "orders_and_fulfillment_admin_reports", :via => [:get, :post] + match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post] match '/admin/products/bulk_edit' => 'admin/products#bulk_edit', :as => "bulk_edit_admin_products" match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management" match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post] diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index a1f1bcfec3..327ce7590a 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -14,17 +14,19 @@ feature %q{ create(:distributor_enterprise) ]) end - it "should not show the Sales Total report" do + it "does not show super admin only reports" do login_to_admin_as user click_link "Reports" page.should_not have_content "Sales Total" + page.should_not have_content "Users & Enterprises" end end context "As an admin user" do - it "shows the Sales Total report" do + it "shows the super admin only reports" do login_to_admin_section click_link "Reports" page.should have_content "Sales Total" + page.should have_content "Users & Enterprises" end end end From 66a3410087c556e0877dc61f854024f6872bfcd6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 13 Nov 2014 12:49:50 +1100 Subject: [PATCH 213/681] WIP: Adding new report to ability spec --- spec/models/spree/ability_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 1e793239ac..4a8f34a1d9 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -209,7 +209,7 @@ module Spree end it "should not be able to read other reports" do - should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors], for: :report) + should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises], for: :report) end end @@ -304,7 +304,7 @@ module Spree end it "should not be able to read other reports" do - should_not have_ability([:sales_total], for: :report) + should_not have_ability([:sales_total, :users_and_enterprises], for: :report) end end From 79a83ee206f5d1c2a6359a3f3ac1a2fb853f0cc8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 14 Nov 2014 12:52:19 +1100 Subject: [PATCH 214/681] Adding users and enterprises report, controller action and template --- .../admin/reports_controller_decorator.rb | 10 ++- .../reports/users_and_enterprises.html.haml | 39 +++++++++++ .../users_and_enterprises_report.rb | 69 +++++++++++++++++++ .../users_and_enterprises_report.rb | 56 +++++++++++++++ 4 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 app/views/spree/admin/reports/users_and_enterprises.html.haml create mode 100644 lib/open_food_network/users_and_enterprises_report.rb create mode 100644 spec/lib/open_food_network/users_and_enterprises_report.rb diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 038e12020a..622edbe9a9 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -4,6 +4,7 @@ require 'open_food_network/products_and_inventory_report' require 'open_food_network/group_buy_report' require 'open_food_network/order_grouper' require 'open_food_network/customers_report' +require 'open_food_network/users_and_enterprises_report' Spree::Admin::ReportsController.class_eval do @@ -572,13 +573,16 @@ Spree::Admin::ReportsController.class_eval do def products_and_inventory @report_types = REPORT_TYPES[:products_and_inventory] - @report = OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params - #@table = @report.table - #@header = @report.header render_report(@report.header, @report.table, params[:csv], "products_and_inventory.csv") end + def users_and_enterprises + # @report_types = REPORT_TYPES[:users_and_enterprises] + @report = OpenFoodNetwork::UsersAndEnterprisesReport.new params + render_report(@report.header, @report.table, params[:csv], "users_and_enterprises.csv") + end + def render_report (header, table, create_csv, csv_file_name) unless create_csv render :html => table diff --git a/app/views/spree/admin/reports/users_and_enterprises.html.haml b/app/views/spree/admin/reports/users_and_enterprises.html.haml new file mode 100644 index 0000000000..d6b4332d3a --- /dev/null +++ b/app/views/spree/admin/reports/users_and_enterprises.html.haml @@ -0,0 +1,39 @@ +-# = form_tag spree.users_and_enterprises_reports_url do |f| +-# %br +-# = label_tag nil, "Enterprise: " +-# = select_tag(:distributor_id, +-# options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), +-# :include_blank => true) +-# +-# %br +-# = label_tag nil, "User: " +-# = select_tag(:supplier_id, +-# options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), +-# :include_blank => true) +-# +-# -# Might need this later if we add different kinds of reports +-# -# %br +-# -# = label_tag nil, "Report Type: " +-# -# = select_tag(:report_type, options_for_select(@report_types, params[:report_type])) +-# +-# %br +-# %br +-# = check_box_tag :csv +-# = label_tag :csv, "Download as csv" +-# %br +-# = button t(:search) +-# %br +-# %br +%table + %thead + %trs + - @report.header.each do |heading| + %th=heading + %tbody + - @report.table.each do |row| + %tr + - row.each do |column| + %td= column + - if @report.table.empty? + %tr + %td{:colspan => "2"}= t(:none) \ No newline at end of file diff --git a/lib/open_food_network/users_and_enterprises_report.rb b/lib/open_food_network/users_and_enterprises_report.rb new file mode 100644 index 0000000000..e3686cbacf --- /dev/null +++ b/lib/open_food_network/users_and_enterprises_report.rb @@ -0,0 +1,69 @@ +module OpenFoodNetwork + class UsersAndEnterprisesReport + attr_reader :params + def initialize(params = {}) + @params = params + end + + def header + [ + "User", + "Relationship", + "Enterprise", + "Producer?", + "Sells", + "Confirmation Date" + ] + end + + def table + users_and_enterprises.map do |uae| [ + uae["user_email"], + uae["relationship_type"], + uae["name"], + to_bool(uae["is_primary_producer"]), + uae["sells"], + to_local_datetime(uae["confirmed_at"]) + ] + end + end + + def owners_and_enterprises + ActiveRecord::Base.connection.execute("SELECT enterprises.name, enterprises.sells, enterprises.is_primary_producer, enterprises.confirmed_at, + 'owns' AS relationship_type, owners.email as user_email FROM enterprises + LEFT JOIN spree_users AS owners ON owners.id=enterprises.owner_id ORDER BY enterprises.name DESC" ) + .to_a + end + + def managers_and_enterprises + ActiveRecord::Base.connection.execute("SELECT enterprises.name, enterprises.sells, enterprises.is_primary_producer, enterprises.confirmed_at, + 'manages' AS relationship_type, managers.email as user_email FROM enterprises + LEFT JOIN enterprise_roles ON enterprises.id=enterprise_roles.enterprise_id + LEFT JOIN spree_users AS managers ON enterprise_roles.user_id=managers.id + WHERE enterprise_id IS NOT NULL + AND user_id IS NOT NULL + ORDER BY enterprises.name DESC, user_email DESC") + .to_a + end + + def users_and_enterprises + sort( owners_and_enterprises.concat managers_and_enterprises ) + end + + def sort(results) + results.sort do |a,b| + [ a["name"], b["relationship_type"], a["user_email"] ] <=> + [ b["name"], a["relationship_type"], b["user_email"] ] + end + end + + def to_bool(value) + ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value) + end + + def to_local_datetime(string) + return "Not Confirmed" if string.nil? + string.to_datetime.in_time_zone.strftime "%Y-%m-%d %H:%M" + end + end +end \ No newline at end of file diff --git a/spec/lib/open_food_network/users_and_enterprises_report.rb b/spec/lib/open_food_network/users_and_enterprises_report.rb new file mode 100644 index 0000000000..11de231871 --- /dev/null +++ b/spec/lib/open_food_network/users_and_enterprises_report.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + + +module OpenFoodNetwork + describe OrderAndDistributorReport do + + describe "users_and_enterprises" do + let!(:owners_and_enterprises) { double(:owners_and_enterprises) } + let!(:managers_and_enterprises) { double(:managers_and_enterprises) } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new {} } + + before do + subject.stub(:owners_and_enterprises) { owners_and_enterprises } + subject.stub(:managers_and_enterprises) { managers_and_enterprises } + end + + it "should concatenate owner and manager queries" do + expect(subject).to receive(:owners_and_enterprises).once + expect(subject).to receive(:managers_and_enterprises).once + expect(owners_and_enterprises).to receive(:concat).with(managers_and_enterprises).and_return [] + expect(subject).to receive(:sort).with [] + subject.users_and_enterprises + end + end + + describe "sorting results" do + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new {} } + + it "sorts by name first" do + uae_mock = [ + { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "bbb" }, + { "name" => "bbb", "relationship_type" => "aaa", "user_email" => "aaa" } + ] + expect(subject.sort uae_mock).to eq [ uae_mock[0], uae_mock[1] ] + end + + it "sorts by relationship type (reveresed) second" do + uae_mock = [ + { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "bbb" }, + { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "aaa" }, + { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "aaa" } + ] + expect(subject.sort uae_mock).to eq [ uae_mock[2], uae_mock[0], uae_mock[1] ] + end + + it "sorts by user_email third" do + uae_mock = [ + { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "aaa" }, + { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "aaa" }, + { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "bbb" } + ] + expect(subject.sort uae_mock).to eq [ uae_mock[0], uae_mock[1], uae_mock[2] ] + end + end + end +end \ No newline at end of file From d9d6b7bee44c98d5ca758731ed57c8ba4e4c9bef Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 14 Nov 2014 16:17:36 +1100 Subject: [PATCH 215/681] Adding filtering to Users and Enterprises report --- .../reports/users_and_enterprises.html.haml | 51 +++++++-------- .../users_and_enterprises_report.rb | 24 +++++-- spec/features/admin/reports_spec.rb | 46 ++++++++++++++ .../users_and_enterprises_report.rb | 62 ++++++++++++++++++- 4 files changed, 148 insertions(+), 35 deletions(-) diff --git a/app/views/spree/admin/reports/users_and_enterprises.html.haml b/app/views/spree/admin/reports/users_and_enterprises.html.haml index d6b4332d3a..6820ee04b1 100644 --- a/app/views/spree/admin/reports/users_and_enterprises.html.haml +++ b/app/views/spree/admin/reports/users_and_enterprises.html.haml @@ -1,32 +1,27 @@ --# = form_tag spree.users_and_enterprises_reports_url do |f| --# %br --# = label_tag nil, "Enterprise: " --# = select_tag(:distributor_id, --# options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), --# :include_blank => true) --# --# %br --# = label_tag nil, "User: " --# = select_tag(:supplier_id, --# options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), --# :include_blank => true) --# --# -# Might need this later if we add different kinds of reports --# -# %br --# -# = label_tag nil, "Report Type: " --# -# = select_tag(:report_type, options_for_select(@report_types, params[:report_type])) --# --# %br --# %br --# = check_box_tag :csv --# = label_tag :csv, "Download as csv" --# %br --# = button t(:search) --# %br --# %br -%table += form_tag spree.users_and_enterprises_admin_reports_url do |f| + .row + .alpha.two.columns= label_tag nil, "Enterprises: " + .omega.fourteen.columns= select_tag(:enterprise_id_in, options_from_collection_for_select(Enterprise.all, :id, :name, params[:enterprise_id_in].andand.split(",")), {class: "select2 fullwidth", multiple: true}) + + .row + .alpha.two.columns= label_tag nil, "Users: " + .omega.fourteen.columns= select_tag(:user_id_in, options_from_collection_for_select(Spree::User.all, :id, :email, params[:user_id_in].andand.split(",")), {class: "select2 fullwidth", multiple: true}) + + -# Might need this later if we add different kinds of reports + -# .row + -# .alpha.two.columns= label_tag nil, "Report Type: " + -# .omega.fourteen.columns= select_tag(:report_type, options_for_select(@report_types, params[:report_type])) + + .row + = check_box_tag :csv + = label_tag :csv, "Download as csv" + .row + = button t(:search) +%br +%br +%table#users_and_enterprises %thead - %trs + %tr - @report.header.each do |heading| %th=heading %tbody diff --git a/lib/open_food_network/users_and_enterprises_report.rb b/lib/open_food_network/users_and_enterprises_report.rb index e3686cbacf..a501c505f0 100644 --- a/lib/open_food_network/users_and_enterprises_report.rb +++ b/lib/open_food_network/users_and_enterprises_report.rb @@ -3,6 +3,10 @@ module OpenFoodNetwork attr_reader :params def initialize(params = {}) @params = params + + # Convert arrays of ids to comma delimited strings + @params[:enterprise_id_in] = @params[:enterprise_id_in].join(',') if @params[:enterprise_id_in].kind_of? Array + @params[:user_id_in] = @params[:user_id_in].join(',') if @params[:user_id_in].kind_of? Array end def header @@ -29,21 +33,29 @@ module OpenFoodNetwork end def owners_and_enterprises - ActiveRecord::Base.connection.execute("SELECT enterprises.name, enterprises.sells, enterprises.is_primary_producer, enterprises.confirmed_at, + query = "SELECT enterprises.name, enterprises.sells, enterprises.is_primary_producer, enterprises.confirmed_at, 'owns' AS relationship_type, owners.email as user_email FROM enterprises - LEFT JOIN spree_users AS owners ON owners.id=enterprises.owner_id ORDER BY enterprises.name DESC" ) - .to_a + LEFT JOIN spree_users AS owners ON owners.id=enterprises.owner_id + WHERE enterprises.id IS NOT NULL + #{ params[:enterprise_id_in].present? ? "AND enterprises.id IN (#{ params[:enterprise_id_in] })" : "" } + #{ params[:user_id_in].present? ? "AND owners.id IN (#{ params[:user_id_in] })" : "" } + ORDER BY enterprises.name DESC" + + ActiveRecord::Base.connection.execute(query).to_a end def managers_and_enterprises - ActiveRecord::Base.connection.execute("SELECT enterprises.name, enterprises.sells, enterprises.is_primary_producer, enterprises.confirmed_at, + query = "SELECT enterprises.name, enterprises.sells, enterprises.is_primary_producer, enterprises.confirmed_at, 'manages' AS relationship_type, managers.email as user_email FROM enterprises LEFT JOIN enterprise_roles ON enterprises.id=enterprise_roles.enterprise_id LEFT JOIN spree_users AS managers ON enterprise_roles.user_id=managers.id WHERE enterprise_id IS NOT NULL + #{ params[:enterprise_id_in].present? ? "AND enterprise_id IN (#{ params[:enterprise_id_in] })" : "" } AND user_id IS NOT NULL - ORDER BY enterprises.name DESC, user_email DESC") - .to_a + #{ params[:user_id_in].present? ? "AND user_id IN (#{ params[:user_id_in] })" : "" } + ORDER BY enterprises.name DESC, user_email DESC" + + ActiveRecord::Base.connection.execute(query).to_a end def users_and_enterprises diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 327ce7590a..817f0ad9c4 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -166,4 +166,50 @@ feature %q{ ].sort end end + + describe "users and enterprises report" do + let!(:enterprise1) { create( :enterprise, owner: create_enterprise_user ) } + let!(:enterprise2) { create( :enterprise, owner: create_enterprise_user ) } + let!(:enterprise3) { create( :enterprise, owner: create_enterprise_user ) } + + before do + enterprise3.enterprise_roles.build( user: enterprise1.owner ).save + + login_to_admin_section + click_link 'Reports' + + click_link 'Users & Enterprises' + end + + it "shows users and enterprises report" do + rows = find("table#users_and_enterprises").all("tr") + table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[0..2] } + + table.sort.should == [ + [ "User", "Relationship", "Enterprise" ], + [ enterprise1.owner.email, "owns", enterprise1.name ], + [ enterprise1.owner.email, "manages", enterprise1.name ], + [ enterprise2.owner.email, "owns", enterprise2.name ], + [ enterprise2.owner.email, "manages", enterprise2.name ], + [ enterprise3.owner.email, "owns", enterprise3.name ], + [ enterprise3.owner.email, "manages", enterprise3.name ], + [ enterprise1.owner.email, "manages", enterprise3.name ] + ].sort + end + + it "filters the list" do + select enterprise3.name, from: "enterprise_id_in" + select enterprise1.owner.email, from: "user_id_in" + + click_button "Search" + + rows = find("table#users_and_enterprises").all("tr") + table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[0..2] } + + table.sort.should == [ + [ "User", "Relationship", "Enterprise" ], + [ enterprise1.owner.email, "manages", enterprise3.name ] + ].sort + end + end end diff --git a/spec/lib/open_food_network/users_and_enterprises_report.rb b/spec/lib/open_food_network/users_and_enterprises_report.rb index 11de231871..cf1676492f 100644 --- a/spec/lib/open_food_network/users_and_enterprises_report.rb +++ b/spec/lib/open_food_network/users_and_enterprises_report.rb @@ -1,8 +1,8 @@ require 'spec_helper' - module OpenFoodNetwork describe OrderAndDistributorReport do + include AuthenticationWorkflow describe "users_and_enterprises" do let!(:owners_and_enterprises) { double(:owners_and_enterprises) } @@ -52,5 +52,65 @@ module OpenFoodNetwork expect(subject.sort uae_mock).to eq [ uae_mock[0], uae_mock[1], uae_mock[2] ] end end + + describe "filtering results" do + let!(:enterprise1) { create(:enterprise, owner: create_enterprise_user ) } + let!(:enterprise2) { create(:enterprise, owner: create_enterprise_user ) } + + describe "for owners and enterprises" do + describe "by enterprise id" do + let!(:params) { { enterprise_id_in: [enterprise1.id.to_s] } } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params } + + it "excludes enterprises that are not explicitly requested" do + results = subject.owners_and_enterprises.to_a.map{ |oae| oae["name"] } + expect(results).to include enterprise1.name + expect(results).to_not include enterprise2.name + end + end + + describe "by user id" do + let!(:params) { { user_id_in: [enterprise1.owner.id.to_s] } } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params } + + it "excludes enterprises that are not explicitly requested" do + results = subject.owners_and_enterprises.to_a.map{ |oae| oae["name"] } + expect(results).to include enterprise1.name + expect(results).to_not include enterprise2.name + end + end + end + + describe "for managers and enterprises" do + describe "by enterprise id" do + let!(:params) { { enterprise_id_in: [enterprise1.id.to_s] } } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params } + + it "excludes enterprises that are not explicitly requested" do + results = subject.managers_and_enterprises.to_a.map{ |mae| mae["name"] } + expect(results).to include enterprise1.name + expect(results).to_not include enterprise2.name + end + end + + describe "by user id" do + let!(:manager1) { create_enterprise_user } + let!(:manager2) { create_enterprise_user } + let!(:params) { { user_id_in: [manager1.id.to_s] } } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params } + + before do + enterprise1.enterprise_roles.build(user: manager1).save + enterprise2.enterprise_roles.build(user: manager2).save + end + + it "excludes enterprises whose managers are not explicitly requested" do + results = subject.managers_and_enterprises.to_a.map{ |mae| mae["name"] } + expect(results).to include enterprise1.name + expect(results).to_not include enterprise2.name + end + end + end + end end end \ No newline at end of file From ffb915ccc5d094951386d33d9d056371cb11e316 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 14 Nov 2014 16:48:33 +1100 Subject: [PATCH 216/681] Orders page doesn't crash when orders have no distributor --- .../admin/orders/index/add_distributor_td.html.haml.deface | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface b/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface index af19c08ddd..5d9b7ab083 100644 --- a/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface +++ b/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface @@ -1,4 +1,4 @@ / insert_top "[data-hook='admin_orders_index_rows']" %td.align-center - = order.distributor.name + = order.distributor.andand.name From 27d1886d534f9d3d3e52564cb3e6f8bbcbedf404 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 14 Nov 2014 17:58:13 +1100 Subject: [PATCH 217/681] Fix mailto link --- app/views/shared/mailers/_social_and_contact.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/mailers/_social_and_contact.html.haml b/app/views/shared/mailers/_social_and_contact.html.haml index 50abe98ebd..3fada4a045 100644 --- a/app/views/shared/mailers/_social_and_contact.html.haml +++ b/app/views/shared/mailers/_social_and_contact.html.haml @@ -23,7 +23,7 @@ Email us: %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %a{:href => "hello@openfoodnetwork.org", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;"} + %a{:href => "mailto:hello@openfoodnetwork.org", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;"} hello@openfoodnetwork.org / /column 2 %span.clear{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block;clear: both;"} From b320f717712ce613e3e5e9d8fde7ebf3f7c2b2f4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 14 Nov 2014 17:59:56 +1100 Subject: [PATCH 218/681] Adding revamp for signup confirmation by @summerscope --- app/mailers/spree/user_mailer_decorator.rb | 1 + .../user_mailer/signup_confirmation.html.haml | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 app/views/spree/user_mailer/signup_confirmation.html.haml diff --git a/app/mailers/spree/user_mailer_decorator.rb b/app/mailers/spree/user_mailer_decorator.rb index dd21a3d0d7..7249c9f820 100644 --- a/app/mailers/spree/user_mailer_decorator.rb +++ b/app/mailers/spree/user_mailer_decorator.rb @@ -1,4 +1,5 @@ Spree::UserMailer.class_eval do + layout 'mailer' def signup_confirmation(user) @user = user diff --git a/app/views/spree/user_mailer/signup_confirmation.html.haml b/app/views/spree/user_mailer/signup_confirmation.html.haml new file mode 100644 index 0000000000..7b9860d708 --- /dev/null +++ b/app/views/spree/user_mailer/signup_confirmation.html.haml @@ -0,0 +1,43 @@ +%h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} +Hello! +%p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} + = "Welcome to #{ Spree::Config[:site_name] }!" + +/ Heading Panel +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   +%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Your login +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + Your login email is + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = @user.email +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + You can start shopping online now at + %a{:href => "#{ spree.root_url }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + -# Remove http:// and trailing slashes from root url if they exist + = spree.root_url.sub(/http:\/\//,"").sub(/\/$/,"") +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   +%hr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   +%p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} + Thanks for joining the network. We look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + We welcome all your questions and feedback; you can use the + %em{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Send Feedback button on the site or email us at + %a{:href => "mailto:hello@openfoodnetwork.org", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + hello@openfoodnetwork.org +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + Cheers, + %br + = "#{ Spree::Config[:site_name] } Team" +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   + += render 'shared/mailers/social_and_contact' From d182bb7bf72b77157d1c82a7e1d7148bdf3637f9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 14 Nov 2014 18:09:48 +1100 Subject: [PATCH 219/681] Add signoff partial for mailer templates --- app/views/shared/mailers/_signoff.html.haml | 8 ++++++++ .../user_mailer/signup_confirmation.html.haml | 19 +++++-------------- 2 files changed, 13 insertions(+), 14 deletions(-) create mode 100644 app/views/shared/mailers/_signoff.html.haml diff --git a/app/views/shared/mailers/_signoff.html.haml b/app/views/shared/mailers/_signoff.html.haml new file mode 100644 index 0000000000..2f2816ac3f --- /dev/null +++ b/app/views/shared/mailers/_signoff.html.haml @@ -0,0 +1,8 @@ +%p   + +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + Cheers, + %br + = "#{ Spree::Config[:site_name] } Team" + +%p   \ No newline at end of file diff --git a/app/views/spree/user_mailer/signup_confirmation.html.haml b/app/views/spree/user_mailer/signup_confirmation.html.haml index 7b9860d708..41ead24c2b 100644 --- a/app/views/spree/user_mailer/signup_confirmation.html.haml +++ b/app/views/spree/user_mailer/signup_confirmation.html.haml @@ -4,8 +4,7 @@ Hello! = "Welcome to #{ Spree::Config[:site_name] }!" / Heading Panel -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   +%p   %p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} Your login @@ -18,11 +17,9 @@ Hello! %a{:href => "#{ spree.root_url }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} -# Remove http:// and trailing slashes from root url if they exist = spree.root_url.sub(/http:\/\//,"").sub(/\/$/,"") -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   +%p   %hr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   +%p   %p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} Thanks for joining the network. We look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} @@ -31,13 +28,7 @@ Hello! Send Feedback button on the site or email us at %a{:href => "mailto:hello@openfoodnetwork.org", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} hello@openfoodnetwork.org -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - Cheers, - %br - = "#{ Spree::Config[:site_name] } Team" -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   + += render 'shared/mailers/signoff' = render 'shared/mailers/social_and_contact' From 0907e2d886c411804ed8251dbf0c67d8f82e3565 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 14 Nov 2014 18:10:40 +1100 Subject: [PATCH 220/681] A few alterations to the enterprise email confirmation email --- .../confirmation_instructions.html.haml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/enterprise_mailer/confirmation_instructions.html.haml b/app/views/enterprise_mailer/confirmation_instructions.html.haml index bc2de61370..3ea65a2048 100644 --- a/app/views/enterprise_mailer/confirmation_instructions.html.haml +++ b/app/views/enterprise_mailer/confirmation_instructions.html.haml @@ -1,20 +1,21 @@ %h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} = "Hi, #{@resource.contact}!" %p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} - = "Please confirm email address for your enterprise " + = "Please confirm your email address for " %strong = "#{@resource.name}." %p   / Callout Panel %p.callout{:style => "margin: 0; padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - Click the link below to confirm email and to activate your enterprise. This link can be used only once: + Click the link below to confirm your email and to activate your enterprise. This link can be used only once: %br %strong = link_to 'Confirm this email address »', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) / /Callout Panel %p   %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - We're so excited that you're joining the Open Food Network! Don't hestitate to get in touch if you have any questions. -%p   + = "We're so excited that you're joining the #{ Spree::Config[:site_name] }! Don't hestitate to get in touch if you have any questions." + += render 'shared/mailers/signoff' = render 'shared/mailers/social_and_contact' From 8d1facb7c39ab0db3daca82f1ab2c1c5409a0a6d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 14 Nov 2014 18:42:16 +1100 Subject: [PATCH 221/681] Adding revamp of enterprise welcome email by @summerscope --- app/views/enterprise_mailer/welcome.html.haml | 77 ++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index acd55b37fd..8d3dc13587 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -1,25 +1,66 @@ %h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} = "Welcome, #{@enterprise.contact}!" %p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} - %strong - = "#{@enterprise.name}" - = " is now on #{Spree::Config[:site_name]}." - -%p   - -/ Callout Panel -%p.callout{:style => "margin: 0; padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - = "Click the link below to update your profile or manage other details for " - %strong - = "#{@enterprise.name}." - %br - %strong - = link_to "Manage #{@enterprise.name} »", spree.admin_url - -%p   - + Congratulations, + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %strong= @enterprise.name + = "is now part of #{ Spree::Config.site_name }!" +/ Heading Panel %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - We're so excited that you're joining the Open Food Network! Don't hestitate to get in touch if you have any questions. + Please find below all the details for viewing and editing your enterprise on + %strong= "#{ Spree::Config.site_name }." + We suggest keeping this email and information somewhere safe. Logging in with the account details below will allow complete access to your products and services. %p   +%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Your enterprise details +%table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;", :width => "100%"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Shop URL + %td   + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %a{:href => "#{ main_app.shop_enterprise_url(@enterprise) }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + = main_app.shop_enterprise_url(@enterprise) + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td   + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Email + %td   + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %a{:href => "mailto: #{ @enterprise.email }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + = @enterprise.email + +%p   +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + Log into + %strong= "#{ Spree::Config.site_name } Admin" + in order to edit your enterprise details such as website and social media links, or to start adding products to your enterprise! + +%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + OFN Admin +%table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;", :width => "100%"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Admin + %td   + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %a{:href => "#{ spree.admin_url }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + = spree.admin_url + +%p   +/ /Heading Panel +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + We're so pleased to have you as a valued member of + %strong= "#{Spree::Config.site_name}!" + Don't hestitate to get in touch if you have any questions. + += render 'shared/mailers/signoff' + = render 'shared/mailers/social_and_contact' \ No newline at end of file From e89184de02540e2b0b1dba7b804df8db082c99fb Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Fri, 14 Nov 2014 14:24:22 +0000 Subject: [PATCH 222/681] 286: Last sneaky dollar sign. Updated to use spree_number_to_currency --- app/views/spree/order_mailer/confirm_email.text.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/order_mailer/confirm_email.text.haml b/app/views/spree/order_mailer/confirm_email.text.haml index c9ad50fb63..719201b6ae 100644 --- a/app/views/spree/order_mailer/confirm_email.text.haml +++ b/app/views/spree/order_mailer/confirm_email.text.haml @@ -9,7 +9,7 @@ Order for: #{@order.bill_address.full_name} - @order.line_items.each do |item| #{item.variant.sku} #{raw(item.variant.product.supplier.name)} #{raw(item.variant.product.name)} #{raw(item.variant.options_text)} (QTY: #{item.quantity}) @ #{item.single_money} = #{item.display_amount} ============================================================ -Subtotal: #{number_to_currency checkout_cart_total_with_adjustments(@order)} +Subtotal: #{spree_number_to_currency checkout_cart_total_with_adjustments(@order)} - checkout_adjustments_for_summary(@order, exclude: [:distribution]).each do |adjustment| #{raw(adjustment.label)} #{adjustment.display_amount} Order Total: #{@order.display_total} From 03b59eae75dda5cbb945bd35cecfb56db870d021 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Fri, 14 Nov 2014 15:56:07 +0000 Subject: [PATCH 223/681] 266: Updating with rohans suggestions to tidy up and 'rubify' the code. Thanks for the tips Rohan! --- .../admin/reports_controller_decorator.rb | 2 +- app/helpers/spree/reports_helper.rb | 20 ++----------------- .../order_cycle_management_report.rb | 4 ++-- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 7ffb32d097..b1deb5c3ed 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -631,7 +631,7 @@ Spree::Admin::ReportsController.class_eval do :orders_and_fulfillment => {:name => "Orders & Fulfillment Reports", :description => ''}, :customers => {:name => "Customers", :description => 'Customer details'}, :products_and_inventory => {:name => "Products & Inventory", :description => ''}, - :order_cycle_management => {:name => "UK Order Cycle Management", :description => ''} + :order_cycle_management => {:name => "Order Cycle Management", :description => ''} } # Return only reports the user is authorized to view. diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index 967cd7fd36..5860625991 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -8,28 +8,12 @@ module Spree end end - #lin-d-hop - #Find the payment methods options for reporting. - #I don't like that this is done in two loops, but redundant list entries - # were created otherwise... def report_payment_method_options(orders) - payment_method_list = [] - orders.map do |o| - payment_method_list << o.payments.first.payment_method.andand.name - end - payment_method_list.uniq.each do |pm| - [ "#{pm}".html_safe, pm] - end + orders.map { |o| o.payments.first.payment_method.andand.name }.uniq end def report_distribution_options(orders) - distribution_list = [] - orders.map do |o| - distribution_list << o.shipping_method.andand.name - end - distribution_list.uniq.each do |pm| - [ "#{pm}".html_safe, pm] - end + orders.map { |o| o.shipping_method.andand.name }.uniq end end diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index fb4da5468d..2087f901de 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -44,7 +44,7 @@ module OpenFoodNetwork end def filter_to_payment_method (orders) - if params.has_key? (:payment_method_name) + if params[:payment_method_name].present? orders.with_payment_method_name(params[:payment_method_name]) else orders @@ -52,7 +52,7 @@ module OpenFoodNetwork end def filter_to_distribution (orders) - if params.has_key? (:distribution_name) + if params[:distribution_name].present? orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:distribution_name]) else orders From cc0da142b72291c0452260a6618c6d47892e2244 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Tue, 18 Nov 2014 19:28:49 +0000 Subject: [PATCH 224/681] 267: Adding to new product form ability to add shipping category to record frozen/chilled --- .../spree/admin/products/new/replace_form.html.haml.deface | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface index 7c627eec4e..be664baf5b 100644 --- a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface +++ b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface @@ -36,8 +36,10 @@ = f.label :product_variant_unit_name, t(:unit_name) %input.fullwidth{ id: 'product_variant_unit_name','ng-model' => 'product.variant_unit_name', :name => 'product[variant_unit_name]', :placeholder => 'eg. bunches', :type => 'text' } .twelve.columns.alpha - .six.columns.alpha + .three.columns.alpha = render 'spree/admin/products/primary_taxon_form', f: f + .three.columns.alpha + = render 'spree/admin/products/shipping_category_form', f: f .three.columns = f.field_container :price do = f.label :price, t(:price) From b81bf60dc2c4d7e7e46cca7774631c29e15bdebe Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Tue, 18 Nov 2014 19:30:44 +0000 Subject: [PATCH 225/681] 267: Cont. Didn't realise commit -a doesn't add a file --- .../spree/admin/products/_shipping_category_form.html.haml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 app/views/spree/admin/products/_shipping_category_form.html.haml diff --git a/app/views/spree/admin/products/_shipping_category_form.html.haml b/app/views/spree/admin/products/_shipping_category_form.html.haml new file mode 100644 index 0000000000..8460f2c32b --- /dev/null +++ b/app/views/spree/admin/products/_shipping_category_form.html.haml @@ -0,0 +1,4 @@ += f.field_container :shipping_categories do + = f.label :shipping_category_id, t(:shipping_categories) + = f.collection_select(:shipping_category_id, Spree::ShippingCategory.all, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth'}) + = f.error_message_on :shipping_category_id From c9d0721acd288649b559474c80b71b54f3b9f0fb Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 19 Nov 2014 11:03:00 +1100 Subject: [PATCH 226/681] Rewire order confirmation email so that two separate emails are sent, one for customers and one for shop owners --- app/mailers/spree/order_mailer_decorator.rb | 20 ++++++++++----- app/models/spree/order_decorator.rb | 19 +++++++++++--- spec/mailers/order_mailer_spec.rb | 28 +++++++++++++-------- spec/models/spree/order_spec.rb | 20 ++++++++++++--- 4 files changed, 62 insertions(+), 25 deletions(-) diff --git a/app/mailers/spree/order_mailer_decorator.rb b/app/mailers/spree/order_mailer_decorator.rb index a43399bd95..324e3e7ef9 100644 --- a/app/mailers/spree/order_mailer_decorator.rb +++ b/app/mailers/spree/order_mailer_decorator.rb @@ -2,14 +2,22 @@ Spree::OrderMailer.class_eval do helper HtmlHelper helper CheckoutHelper helper SpreeCurrencyHelper - def confirm_email(order, resend = false) - find_order(order) + def confirm_email_for_customer(order, resend = false) + find_order(order) # Finds an order instance from an id subject = (resend ? "[#{t(:resend).upcase}] " : '') subject += "#{Spree::Config[:site_name]} #{t('order_mailer.confirm_email.subject')} ##{@order.number}" - mail(:to => @order.email, - :from => from_address, + mail(:to => @order.email, + :from => from_address, :subject => subject, - :reply_to => @order.distributor.email, - :cc => @order.distributor.email) + :reply_to => @order.distributor.email) + end + + def confirm_email_for_shop(order, resend = false) + find_order(order) # Finds an order instance from an id + subject = (resend ? "[#{t(:resend).upcase}] " : '') + subject += "#{Spree::Config[:site_name]} #{t('order_mailer.confirm_email.subject')} ##{@order.number}" + mail(:to => @order.distributor.email, + :from => from_address, + :subject => subject) end end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index fcc6054f3b..3396ebde8e 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -21,7 +21,7 @@ Spree::Order.class_eval do go_to_state :delivery go_to_state :payment, :if => lambda { |order| # Fix for #2191 - if order.shipping_method.andand.require_ship_address and + if order.shipping_method.andand.require_ship_address and if order.ship_address.andand.valid? order.create_shipment! order.update_totals @@ -76,10 +76,10 @@ Spree::Order.class_eval do errors.add(:distributor_id, "cannot supply the products in your cart") unless DistributionChangeValidator.new(self).can_change_to_distributor?(distributor) end end - + def empty_with_clear_shipping_and_payments! empty_without_clear_shipping_and_payments! - payments.clear + payments.clear update_attributes(shipping_method_id: nil) end alias_method_chain :empty!, :clear_shipping_and_payments @@ -170,7 +170,7 @@ Spree::Order.class_eval do # Show payment methods for this distributor def available_payment_methods - @available_payment_methods ||= Spree::PaymentMethod.available(:front_end).select do |pm| + @available_payment_methods ||= Spree::PaymentMethod.available(:front_end).select do |pm| (self.distributor && (pm.distributors.include? self.distributor)) end end @@ -179,6 +179,17 @@ Spree::Order.class_eval do Spree::ShippingMethod.all_available(self, display_on) end + # Overrride of Spree method, that allows us to send separate confirmation emails to user and shop owners + def deliver_order_confirmation_email + begin + Spree::OrderMailer.confirm_email_for_customer(self.id).deliver + Spree::OrderMailer.confirm_email_for_shop(self.id).deliver + rescue Exception => e + logger.error("#{e.class.name}: #{e.message}") + logger.error(e.backtrace * "\n") + end + end + private diff --git a/spec/mailers/order_mailer_spec.rb b/spec/mailers/order_mailer_spec.rb index 0fae742ef9..3b8de23e24 100644 --- a/spec/mailers/order_mailer_spec.rb +++ b/spec/mailers/order_mailer_spec.rb @@ -20,18 +20,24 @@ describe Spree::OrderMailer do ActionMailer::Base.deliveries = [] end - it "should send an email when given an order" do - Spree::OrderMailer.confirm_email(@order1.id).deliver - ActionMailer::Base.deliveries.count.should == 1 + describe "order confirmation for customers" do + it "should send an email to the customer when given an order" do + Spree::OrderMailer.confirm_email_for_customer(@order1.id).deliver + ActionMailer::Base.deliveries.count.should == 1 + ActionMailer::Base.deliveries.first.to.should == [@order1.email] + end + + it "sets a reply-to of the enterprise email" do + Spree::OrderMailer.confirm_email_for_customer(@order1.id).deliver + ActionMailer::Base.deliveries.first.reply_to.should == [@distributor.email] + end end - it "sets a reply-to of the enterprise email" do - Spree::OrderMailer.confirm_email(@order1.id).deliver - ActionMailer::Base.deliveries.first.reply_to.should == [@distributor.email] - end - - it "ccs the enterprise" do - Spree::OrderMailer.confirm_email(@order1.id).deliver - ActionMailer::Base.deliveries.first.cc.should == [@distributor.email] + describe "order confirmation for shops" do + it "sends an email to the shop owner when given an order" do + Spree::OrderMailer.confirm_email_for_shop(@order1.id).deliver + ActionMailer::Base.deliveries.count.should == 1 + ActionMailer::Base.deliveries.first.to.should == [@distributor.email] + end end end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 63c315a18a..3322a1672e 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -27,13 +27,13 @@ describe Spree::Order do let(:order) { build(:order, distributor: order_distributor) } let(:pm1) { create(:payment_method, distributors: [order_distributor])} let(:pm2) { create(:payment_method, distributors: [some_other_distributor])} - + it "finds the correct payment methods" do - Spree::PaymentMethod.stub(:available).and_return [pm1, pm2] + Spree::PaymentMethod.stub(:available).and_return [pm1, pm2] order.available_payment_methods.include?(pm2).should == false order.available_payment_methods.include?(pm1).should == true end - + end describe "updating the distribution charge" do @@ -245,7 +245,7 @@ describe Spree::Order do subject.should_not_receive(:empty!) subject.set_order_cycle! subject.order_cycle end - + it "sets the order cycle when no distributor is set" do subject.set_order_cycle! oc subject.order_cycle.should == oc @@ -366,4 +366,16 @@ describe Spree::Order do end end + describe "sending confirmation emails" do + it "sends confirmation emails to both the user and the shop owner" do + customer_confirm_fake = double(:confirm_email_for_customer) + shop_confirm_fake = double(:confirm_email_for_shop) + expect(Spree::OrderMailer).to receive(:confirm_email_for_customer).and_return customer_confirm_fake + expect(Spree::OrderMailer).to receive(:confirm_email_for_shop).and_return shop_confirm_fake + expect(customer_confirm_fake).to receive :deliver + expect(shop_confirm_fake).to receive :deliver + create(:order).deliver_order_confirmation_email + end + end + end From dd6a5ecd0b5019222e49f1032337ae6fee37e131 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 19 Nov 2014 16:42:31 +1100 Subject: [PATCH 227/681] Adding helper for assets in mailer views, fixing links in mailer layout --- app/helpers/spree/mailer_helper.rb | 7 +++++++ app/mailers/enterprise_mailer.rb | 1 + app/mailers/spree/user_mailer_decorator.rb | 1 + app/views/layouts/mailer.html.haml | 13 +++++++------ 4 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 app/helpers/spree/mailer_helper.rb diff --git a/app/helpers/spree/mailer_helper.rb b/app/helpers/spree/mailer_helper.rb new file mode 100644 index 0000000000..f0e4a777a1 --- /dev/null +++ b/app/helpers/spree/mailer_helper.rb @@ -0,0 +1,7 @@ +module Spree + module MailerHelper + def email_asset_url(asset) + URI.join(root_url, asset_path(asset)).to_s + end + end +end \ No newline at end of file diff --git a/app/mailers/enterprise_mailer.rb b/app/mailers/enterprise_mailer.rb index 7504597589..dc7b9d8695 100644 --- a/app/mailers/enterprise_mailer.rb +++ b/app/mailers/enterprise_mailer.rb @@ -3,6 +3,7 @@ class EnterpriseMailer < Spree::BaseMailer include Devise::Mailers::Helpers layout 'mailer' + helper Spree::MailerHelper def welcome(enterprise) @enterprise = enterprise diff --git a/app/mailers/spree/user_mailer_decorator.rb b/app/mailers/spree/user_mailer_decorator.rb index 7249c9f820..3663212299 100644 --- a/app/mailers/spree/user_mailer_decorator.rb +++ b/app/mailers/spree/user_mailer_decorator.rb @@ -1,5 +1,6 @@ Spree::UserMailer.class_eval do layout 'mailer' + helper Spree::MailerHelper def signup_confirmation(user) @user = user diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 74d05ad6b9..8477e3717e 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -3,8 +3,8 @@ / If you delete this meta tag, Half Life 3 will never be released. %meta{:content => "width=device-width", :name => "viewport", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ - %title{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} Open Food Network - %link{:href => "http://rohanmitchell.com/random/template/basic-email-template/stylesheets/email.css", :rel => "stylesheet", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :type => "text/css"}/ + %title{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Open Food Network %body{:bgcolor => "#FFFFFF", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-webkit-text-size-adjust: none;height: 100%;width: 100%!important;"} / HEADER %table.head-wrap{:bgcolor => "#333333", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} @@ -15,9 +15,10 @@ %table{:bgcolor => "#333333", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %img{:src => "https://openfoodnetwork.org.au/assets/ofn_logo_beta-8e4dfc79deb25def2d107dea52dce492.png", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 100%;", :width => "200"}/ + %img{:src => "#{ email_asset_url 'open-food-network-beta.png' }", srcset: "#{ email_asset_url 'open-food-network-beta.svg' }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 100%;", :width => "200"}/ %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %h6.collapse{:style => "margin: 0!important;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #999;font-weight: 900;font-size: 14px;text-transform: uppercase;"} Open Food Network + %h6.collapse{:style => "margin: 0!important;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #999;font-weight: 900;font-size: 14px;text-transform: uppercase;"} + Open Food Network %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} / /HEADER / BODY @@ -45,10 +46,10 @@ %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} %td{:align => "center", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - %a{:href => "https://openfoodnetwork.org.au/Terms-of-service.pdf", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + %a{:href => "#{ URI.join(spree.root_url, "Terms-of-service.pdf").to_s }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} Terms of service | - %a{:href => "http://www.openfoodnetwork.org.au", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + %a{:href => "#{ spree.root_url }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} Open Food Network / | Unsubscribe / /content From 20341ecbc1387958e76d4b95e25eae59ad554b77 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 12:04:51 +1100 Subject: [PATCH 228/681] Send bugsnag notification when order confirmation email fails --- app/models/spree/order_decorator.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 3396ebde8e..d8b33cd4f0 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -185,6 +185,7 @@ Spree::Order.class_eval do Spree::OrderMailer.confirm_email_for_customer(self.id).deliver Spree::OrderMailer.confirm_email_for_shop(self.id).deliver rescue Exception => e + Bugsnag.notify(e) logger.error("#{e.class.name}: #{e.message}") logger.error(e.backtrace * "\n") end From fe0bb49bafcdc046ca6e06eb365c8a6a241e7149 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 12:06:53 +1100 Subject: [PATCH 229/681] WIP: Adding templates for different order confirmation emails --- app/mailers/spree/order_mailer_decorator.rb | 3 + .../confirm_email_for_customer.html.haml | 178 ++++++++++++++++++ .../confirm_email_for_shop.html.haml | 139 ++++++++++++++ 3 files changed, 320 insertions(+) create mode 100644 app/views/spree/order_mailer/confirm_email_for_customer.html.haml create mode 100644 app/views/spree/order_mailer/confirm_email_for_shop.html.haml diff --git a/app/mailers/spree/order_mailer_decorator.rb b/app/mailers/spree/order_mailer_decorator.rb index 324e3e7ef9..e2c63257ae 100644 --- a/app/mailers/spree/order_mailer_decorator.rb +++ b/app/mailers/spree/order_mailer_decorator.rb @@ -1,7 +1,10 @@ Spree::OrderMailer.class_eval do + layout 'mailer' + helper Spree::MailerHelper helper HtmlHelper helper CheckoutHelper helper SpreeCurrencyHelper + def confirm_email_for_customer(order, resend = false) find_order(order) # Finds an order instance from an id subject = (resend ? "[#{t(:resend).upcase}] " : '') diff --git a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml new file mode 100644 index 0000000000..50816370e5 --- /dev/null +++ b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml @@ -0,0 +1,178 @@ +%h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} + = "Hi #{@order.bill_address.firstname}," +%table.social.white-bg{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;background-color: white!important;width: 100%;border: 1px solid #ebebeb;", :width => "100%"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + / column 1 + %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = "Order confirmation ##{@order.number}" + %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + Thanks for shopping on + %strong= "#{Spree::Config.site_name}." + Here are your order details from + %strong= "#{@order.distributor.name}." + / /column 1 + / column 2 + %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %img.float-right{:src => "#{@order.distributor.logo.url(:thumb)}", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 100%;float: right;display: block;"}/ + / /column 2 + %span.clear{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block;clear: both;"} +/ /intro + +/ Heading Panel +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   + +%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Order summary +/ /Heading Panel +/ Order Summary +/

    Order for: Rob Harrington

    +%table.order-summary{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;", :width => "100%"} + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Item + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :width => "25%"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Qty + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :width => "25%"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Price + - @order.line_items.each do |item| + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = raw(item.variant.product.supplier.name) + %br + = "#{raw(item.variant.product.name)} #{raw(item.variant.options_text)}" + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = item.quantity + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = item.display_amount + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:colspan => "3", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} +   + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right", :colspan => "2", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Subtotal: + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = spree_number_to_currency checkout_cart_total_with_adjustments(@order) + - checkout_adjustments_for_summary(@order, exclude: [:distribution]).reject{ |a| a.amount == 0 }.each do |adjustment| + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right", :colspan => "2", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = raw(adjustment.label) + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = adjustment.display_amount + %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right", :colspan => "2", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Total: + %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = @order.display_total + +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   + +-# Payment Summary +- if @order.payments.first.andand.payment_method.andand.type == "Spree::PaymentMethod::Check" and @order.payments.first.andand.payment_method.andand.description + / Heading Panel + %p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Payment summary + / /Heading Panel + / Payment Summary + %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + = @order.payments.first.andand.payment_method.andand.description.andand.html_safe + + -# Spacer + %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   + + + +- if @order.shipping_method.andand.require_ship_address + -# Delivery details + %p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Delivery details + %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + Your order will be delivered to: + %br + #{@order.ship_address.to_s} + %br + + - if @order.shipping_method.andand.description + #{@order.shipping_method.description.html_safe} + %br + + - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + Delivery on: #{@order.order_cycle.pickup_time_for(@order.distributor)} + %br + + - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) + Other delivery information: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} + %br + + +- else + -# Collection Details + %p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + Collection details + %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + - if @order.shipping_method.andand.description + = @order.shipping_method.description.html_safe + %br + + - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + Ready for collection: #{@order.order_cycle.pickup_time_for(@order.distributor)} + %br + + - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) + Collection instructions: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} + %br + + - if @order.special_instructions.present? + Notes: #{@order.special_instructions} + %br + + -# Your order will be ready for collection on + -# %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + -# Tuesday 07 Dec + -# %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + -# Pick-up your order at the rear of 34 Mason Street, Warragul. See it on + -# %a{:href => "https://goo.gl/maps/T1ArU", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + -# google maps + +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   +%hr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + Thanks for your support. +%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +   + +-# Contact details +%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + #{@order.distributor.contact}, + %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ + %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + = @order.distributor.name + %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ + %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ + = @order.distributor.phone || "" + %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ + %a{:href => "mailto:#{@order.distributor.email}", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;font-weight: bold;", :target => "_blank"} + = @order.distributor.email + %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ + = @order.distributor.website || "" \ No newline at end of file diff --git a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml new file mode 100644 index 0000000000..8f40e0a148 --- /dev/null +++ b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml @@ -0,0 +1,139 @@ +%h3 Hi #{@order.bill_address.firstname}, +%table.social.white-bg{:width => "100%"} + %tr + %td + %table.column{:align => "left"} + %tr + %td + %p + %strong Order confirmation ##{@order.number} + %p + Thanks for shopping on + %strong= "#{Spree::Config.site_name}." + Here are your order details from + %strong= "#{@order.distributor.name}." + %table.column{:align => "left"} + %tr + %td + %img.float-right{:src => "#{@order.distributor.logo.url(:thumb)}"}/ + %span.clear +%p   + + +%p.callout + %strong Order summary +/ /Heading Panel +/ Order Summary +/

    Order for: Rob Harrington

    +%table.order-summary{:width => "100%"} + %tr + %td + %strong Item + %td{:align => "right", :width => "25%"} + %strong Qty + %td{:align => "right", :width => "25%"} + %strong Price + - @order.line_items.each do |item| + %tr + %td + = raw(item.variant.product.supplier.name) + %br + = "#{raw(item.variant.product.name)} #{raw(item.variant.options_text)}" + %td{:align => "right"} + = item.quantity + %td{:align => "right"} + = item.display_amount + %tr + %td{:colspan => "3"}   + %tr + %td{:align => "right", :colspan => "2"} + Subtotal: + %td{:align => "right"} + = spree_number_to_currency checkout_cart_total_with_adjustments(@order) + - checkout_adjustments_for_summary(@order, exclude: [:distribution]).reject{ |a| a.amount == 0 }.each do |adjustment| + %tr + %td{:align => "right", :colspan => "2"} + = raw(adjustment.label) + %td{:align => "right"} + = adjustment.display_amount + %tr + %td{:align => "right", :colspan => "2"} + %strong Total: + %td{:align => "right"} + %strong= @order.display_total +%p   + +- if @order.payments.first.andand.payment_method.andand.type == "Spree::PaymentMethod::Check" and @order.payments.first.andand.payment_method.andand.description + %p.callout + %strong Payment summary + / /Heading Panel + / Payment Summary + %p= @order.payments.first.andand.payment_method.andand.description.andand.html_safe + %p   + +- if @order.shipping_method.andand.require_ship_address + / Delivery details + %p.callout + %strong + Delivery details + %p + Your order will be delivered to: + %br + #{@order.ship_address.to_s} + %br + + - if @order.shipping_method.andand.description + #{@order.shipping_method.description.html_safe} + %br + + - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + Delivery on: #{@order.order_cycle.pickup_time_for(@order.distributor)} + %br + + - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) + Other delivery information: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} + %br + + +- else + / Collection details + %p.callout + %strong + Collection details + %p + - if @order.shipping_method.andand.description + = @order.shipping_method.description.html_safe + %br + + - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + Ready for collection: #{@order.order_cycle.pickup_time_for(@order.distributor)} + %br + + - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) + Collection instructions: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} + %br + + - if @order.special_instructions.present? + Notes: #{@order.special_instructions} + %br + + -# Your order will be ready for collection on + -# %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + -# Tuesday 07 Dec + -# %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + -# Pick-up your order at the rear of 34 Mason Street, Warragul. See it on + -# %a{:href => "https://goo.gl/maps/T1ArU", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + -# google maps + + +%p.callout + #{@order.distributor.contact}, + %br/ + %strong= @order.distributor.name + %br/ + = @order.distributor.phone || "" + %br/ + %a{:href => "mailto:#{@order.distributor.email}", :target => "_blank"} + = @order.distributor.email + %br/ + = @order.distributor.website || "" \ No newline at end of file From ac6a043f404f036df3eff9a0b77465d3bc66a99b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 14:56:35 +1100 Subject: [PATCH 230/681] Add roadie-rails to help with inlining styles for emails --- Gemfile | 1 + Gemfile.lock | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/Gemfile b/Gemfile index 4fec11abeb..844410e478 100644 --- a/Gemfile +++ b/Gemfile @@ -43,6 +43,7 @@ gem 'spinjs-rails' gem 'rack-ssl', :require => 'rack/ssl' gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg' gem 'angularjs-file-upload-rails', '~> 1.1.0' +gem 'roadie-rails', '~> 1.0.3' gem 'foreigner' gem 'immigrant' diff --git a/Gemfile.lock b/Gemfile.lock index 4af4129c5b..a984220c77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -209,6 +209,8 @@ GEM compass (>= 0.12.2, < 0.14) crack (0.4.1) safe_yaml (~> 0.9.0) + css_parser (1.3.5) + addressable dalli (2.7.2) database_cleaner (0.7.1) db2fog (0.8.0) @@ -419,6 +421,12 @@ GEM representative_view (1.2.2) actionpack (> 2.3.0, < 4.0.0) representative (~> 1.0.2) + roadie (3.0.1) + css_parser (~> 1.3.4) + nokogiri (~> 1.6.0) + roadie-rails (1.0.3) + rails (>= 3.0, < 4.2) + roadie (~> 3.0) rspec (2.14.1) rspec-core (~> 2.14.0) rspec-expectations (~> 2.14.0) @@ -557,6 +565,7 @@ DEPENDENCIES rack-ssl rails (= 3.2.19) representative_view + roadie-rails (~> 1.0.3) rspec-rails sass (~> 3.2) sass-rails (~> 3.2.3) From 2e84a8f6266cc6aaf7674bb371a668085af1b3f0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 15:05:19 +1100 Subject: [PATCH 231/681] Pull layout and roadie style inlining out into spree's base mailer so it applies to all mailers --- app/mailers/enterprise_mailer.rb | 3 --- app/mailers/spree/base_mailer_decorator.rb | 8 ++++++++ app/mailers/spree/order_mailer_decorator.rb | 2 -- app/mailers/spree/user_mailer_decorator.rb | 3 --- 4 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 app/mailers/spree/base_mailer_decorator.rb diff --git a/app/mailers/enterprise_mailer.rb b/app/mailers/enterprise_mailer.rb index dc7b9d8695..8ef7e74044 100644 --- a/app/mailers/enterprise_mailer.rb +++ b/app/mailers/enterprise_mailer.rb @@ -2,9 +2,6 @@ require 'devise/mailers/helpers' class EnterpriseMailer < Spree::BaseMailer include Devise::Mailers::Helpers - layout 'mailer' - helper Spree::MailerHelper - def welcome(enterprise) @enterprise = enterprise mail(:to => enterprise.email, :from => from_address, diff --git a/app/mailers/spree/base_mailer_decorator.rb b/app/mailers/spree/base_mailer_decorator.rb new file mode 100644 index 0000000000..a776eb8946 --- /dev/null +++ b/app/mailers/spree/base_mailer_decorator.rb @@ -0,0 +1,8 @@ +Spree::BaseMailer.class_eval do + # Inline stylesheets + include Roadie::Rails::Automatic + + # Define layout + layout 'mailer' + helper Spree::MailerHelper +end \ No newline at end of file diff --git a/app/mailers/spree/order_mailer_decorator.rb b/app/mailers/spree/order_mailer_decorator.rb index e2c63257ae..c6eeb31df8 100644 --- a/app/mailers/spree/order_mailer_decorator.rb +++ b/app/mailers/spree/order_mailer_decorator.rb @@ -1,6 +1,4 @@ Spree::OrderMailer.class_eval do - layout 'mailer' - helper Spree::MailerHelper helper HtmlHelper helper CheckoutHelper helper SpreeCurrencyHelper diff --git a/app/mailers/spree/user_mailer_decorator.rb b/app/mailers/spree/user_mailer_decorator.rb index 3663212299..40313d1b66 100644 --- a/app/mailers/spree/user_mailer_decorator.rb +++ b/app/mailers/spree/user_mailer_decorator.rb @@ -1,7 +1,4 @@ Spree::UserMailer.class_eval do - layout 'mailer' - helper Spree::MailerHelper - def signup_confirmation(user) @user = user mail(:to => user.email, :from => from_address, From 15d157abc0644e1e977ce61c69b33008b92884f9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 15:42:03 +1100 Subject: [PATCH 232/681] Stripping out inline styles from email templates --- app/assets/stylesheets/mail/all.css | 11 + app/assets/stylesheets/mail/email.css.sass | 280 ++++++++++++++++++ app/views/layouts/mailer.html.haml | 88 +++--- app/views/shared/mailers/_signoff.html.haml | 2 +- .../mailers/_social_and_contact.html.haml | 41 ++- .../confirm_email_for_customer.html.haml | 168 ++++------- .../confirm_email_for_shop.html.haml | 3 - 7 files changed, 415 insertions(+), 178 deletions(-) create mode 100644 app/assets/stylesheets/mail/all.css create mode 100644 app/assets/stylesheets/mail/email.css.sass diff --git a/app/assets/stylesheets/mail/all.css b/app/assets/stylesheets/mail/all.css new file mode 100644 index 0000000000..a4d45352b2 --- /dev/null +++ b/app/assets/stylesheets/mail/all.css @@ -0,0 +1,11 @@ +/* This file MUST be plain css, as roadie does not seem to be able to handle other file types */ + +/* + * This is a manifest file that'll automatically include all the stylesheets available in this directory + * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at + * the top of the compiled file, but it's generally better to create a new file per style scope. + * + + *= require_self + *= require_tree . +*/ diff --git a/app/assets/stylesheets/mail/email.css.sass b/app/assets/stylesheets/mail/email.css.sass new file mode 100644 index 0000000000..3e20fb32c4 --- /dev/null +++ b/app/assets/stylesheets/mail/email.css.sass @@ -0,0 +1,280 @@ +/* ------------------------------------- + * GLOBAL + *------------------------------------- + +* + margin: 0 + padding: 0 + font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif + +img + max-width: 100% + +.collapse + margin: 0 + padding: 0 + +body + -webkit-font-smoothing: antialiased + -webkit-text-size-adjust: none + width: 100%!important + height: 100% + +/* ------------------------------------- + * ELEMENTS + *------------------------------------- + +a + color: #0096ad + +.btn + text-decoration: none + color: #FFF + background-color: #666 + padding: 10px 16px + font-weight: bold + margin-right: 10px + text-align: center + cursor: pointer + display: inline-block + +p.callout + padding: 15px + background-color: #e1f0f5 + margin-bottom: 15px + +.callout a + font-weight: bold + color: #0096ad + +table.social + background-color: #ebebeb + &.white-bg + background-color: white!important + border: 1px solid #ebebeb + +.social .soc-btn + padding: 3px 7px + font-size: 12px + margin-bottom: 10px + text-decoration: none + color: #FFF + font-weight: bold + display: block + text-align: center + +a + &.fb + background-color: #3B5998!important + &.tw + background-color: #1daced!important + &.gp + background-color: #DB4A39!important + &.li + background-color: #0073b2!important + &.ms + background-color: #000!important + +.sidebar .soc-btn + display: block + width: 100% + +img.float-right + float: right + display: block + +/* ------------------------------------- + * HEADER + *------------------------------------- + +table.head-wrap + width: 100% + +.header.container table td + &.logo + padding: 15px + &.label + padding: 15px + padding-left: 0px + +/* ------------------------------------- + * BODY + *------------------------------------- + +table + &.body-wrap + width: 100% + &.footer-wrap + width: 100% + clear: both!important + +/* ------------------------------------- + * FOOTER + *------------------------------------- + +.footer-wrap .container td.content p + border-top: 1px solid rgb(215, 215, 215) + padding-top: 15px + font-size: 10px + font-weight: bold + +/* ------------------------------------- + * TYPOGRAPHY + *------------------------------------- + +h1, h2, h3, h4, h5, h6 + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif + line-height: 1.1 + margin-bottom: 15px + color: #000 + +h1 small, h2 small, h3 small, h4 small, h5 small, h6 small + font-size: 60% + color: #6f6f6f + line-height: 0 + text-transform: none + +h1 + font-weight: 200 + font-size: 44px + +h2 + font-weight: 200 + font-size: 37px + +h3 + font-weight: 500 + font-size: 27px + +h4 + font-weight: 500 + font-size: 23px + +h5 + font-weight: 900 + font-size: 17px + +h6 + font-weight: 900 + font-size: 14px + text-transform: uppercase + color: #999 + +.collapse + margin: 0!important + +p, ul + margin-bottom: 10px + font-weight: normal + font-size: 14px + line-height: 1.6 + +p + &.lead + font-size: 17px + &.last + margin-bottom: 0px + +ul + li + margin-left: 5px + list-style-position: inside + &.sidebar + background: #ebebeb + display: block + list-style-type: none + li + display: block + margin: 0 + a + text-decoration: none + color: #666 + padding: 10px 16px + /* font-weight:bold; + margin-right: 10px + /* text-align:center; + cursor: pointer + border-bottom: 1px solid #777777 + border-top: 1px solid #FFFFFF + display: block + margin: 0 + &.last + border-bottom-width: 0px + h1, h2, h3, h4, h5, h6, p + margin-bottom: 0!important + +/* ------------------------------------- + * SIDEBAR + *------------------------------------- + +/* --------------------------------------------------- + * RESPONSIVENESS + * Nuke it from orbit. It's the only way to be sure. + *------------------------------------------------------ + +/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something + +.container + display: block!important + max-width: 600px!important + margin: 0 auto!important + /* makes it centered + clear: both!important + +/* This should also be a block element, so that it will fill 100% of the .container + +.content + padding: 15px + max-width: 600px + margin: 0 auto + display: block + table + width: 100% + +/* Let's make sure tables in the content area are 100% wide + +/* Odds and ends + +.column + width: 300px + float: left + tr td + padding: 15px + +.column-wrap + padding: 0!important + margin: 0 auto + max-width: 600px!important + +.column table + width: 100% + +.social .column + width: 280px + min-width: 279px + float: left + +/* Be sure to place a .clear element after each set of columns, just to be safe + +.clear + display: block + clear: both + +/* ------------------------------------------- + * PHONE + * For clients that support media queries. + * Nothing fancy. + *-------------------------------------------- +@media only screen and (max-width: 600px) + a[class="btn"] + display: block!important + margin-bottom: 10px!important + background-image: none!important + margin-right: 0!important + div[class="column"] + width: auto!important + float: none!important + table.social div[class="column"] + width: auto!important + img.float-right + float: none!important diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 8477e3717e..1f3dbdb2cf 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -1,57 +1,51 @@ -%html{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :xmlns => "http://www.w3.org/1999/xhtml"} - %head{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} +%html{:xmlns => "http://www.w3.org/1999/xhtml"} + %head / If you delete this meta tag, Half Life 3 will never be released. - %meta{:content => "width=device-width", :name => "viewport", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ - %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ - %title{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %meta{:content => "width=device-width", :name => "viewport" }/ + %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ + %title Open Food Network - %body{:bgcolor => "#FFFFFF", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-webkit-text-size-adjust: none;height: 100%;width: 100%!important;"} - / HEADER - %table.head-wrap{:bgcolor => "#333333", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td.header.container{:style => "margin: 0 auto!important;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block!important;max-width: 600px!important;clear: both!important;"} - .content{:style => "margin: 0 auto;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 600px;display: block;"} - %table{:bgcolor => "#333333", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %img{:src => "#{ email_asset_url 'open-food-network-beta.png' }", srcset: "#{ email_asset_url 'open-food-network-beta.svg' }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 100%;", :width => "200"}/ - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %h6.collapse{:style => "margin: 0!important;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #999;font-weight: 900;font-size: 14px;text-transform: uppercase;"} + = stylesheet_link_tag 'mail/all' + %body{:bgcolor => "#FFFFFF" } + %table.head-wrap{:bgcolor => "#333333"} + %tr + %td + %td.header.container + .content + %table{:bgcolor => "#333333"} + %tr + %td + %img{:src => "#{ email_asset_url 'open-food-network-beta.png' }", srcset: "#{ email_asset_url 'open-food-network-beta.svg' }", :width => "200"}/ + %td{:align => "right"} + %h6.collapse Open Food Network - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - / /HEADER - / BODY - %table.body-wrap{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td.container{:bgcolor => "#FFFFFF", :style => "margin: 0 auto!important;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block!important;max-width: 600px!important;clear: both!important;"} - .content{:style => "margin: 0 auto;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 600px;display: block;"} - %table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td + + %table.body-wrap + %tr + %td + %td.container{:bgcolor => "#FFFFFF"} + .content + %table + %tr + %td = yield + %td - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - / /BODY - / FOOTER - %table.footer-wrap{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;clear: both!important;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td.container{:style => "margin: 0 auto!important;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block!important;max-width: 600px!important;clear: both!important;"} - / content - .content{:style => "margin: 0 auto;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 600px;display: block;"} - %table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:align => "center", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - %a{:href => "#{ URI.join(spree.root_url, "Terms-of-service.pdf").to_s }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + %table.footer-wrap + %tr + %td + %td.container + .content + %table + %tr + %td{:align => "center"} + %p + %a{:href => "#{ URI.join(spree.root_url, "Terms-of-service.pdf").to_s }", :target => "_blank"} Terms of service | - %a{:href => "#{ spree.root_url }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + %a{:href => "#{ spree.root_url }"} Open Food Network / | Unsubscribe - / /content - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - / /FOOTER \ No newline at end of file + %td \ No newline at end of file diff --git a/app/views/shared/mailers/_signoff.html.haml b/app/views/shared/mailers/_signoff.html.haml index 2f2816ac3f..6343ac85ae 100644 --- a/app/views/shared/mailers/_signoff.html.haml +++ b/app/views/shared/mailers/_signoff.html.haml @@ -1,6 +1,6 @@ %p   -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +%p Cheers, %br = "#{ Spree::Config[:site_name] } Team" diff --git a/app/views/shared/mailers/_social_and_contact.html.haml b/app/views/shared/mailers/_social_and_contact.html.haml index 3fada4a045..81244778d6 100644 --- a/app/views/shared/mailers/_social_and_contact.html.haml +++ b/app/views/shared/mailers/_social_and_contact.html.haml @@ -1,29 +1,26 @@ -%table.social{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;background-color: #ebebeb;width: 100%;", :width => "100%"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} +%table.social{ :width => "100%" } + %tr + %td / column 1 - %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %h5{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 900;font-size: 17px;"} + %table.column{:align => "left"} + %tr + %td + %h5 Connect with Us: - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - %a.soc-btn.fb{:href => "https://www.facebook.com/OpenFoodNet", :style => "margin: 0;padding: 3px 7px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #FFF;font-size: 12px;margin-bottom: 10px;text-decoration: none;font-weight: bold;display: block;text-align: center;background-color: #3B5998!important;", :target => "_blank"} + %p + %a.soc-btn.fb{:href => "https://www.facebook.com/OpenFoodNet", :target => "_blank"} Facebook - %a.soc-btn.tw{:href => "https://twitter.com/OpenFoodNet", :style => "margin: 0;padding: 3px 7px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #FFF;font-size: 12px;margin-bottom: 10px;text-decoration: none;font-weight: bold;display: block;text-align: center;background-color: #1daced!important;", :target => "_blank"} + %a.soc-btn.tw{:href => "https://twitter.com/OpenFoodNet", :target => "_blank"} Twitter - %a.soc-btn.li{:href => "http://www.linkedin.com/groups/Open-Food-Foundation-4743336", :style => "margin: 0;padding: 3px 7px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #FFF;font-size: 12px;margin-bottom: 10px;text-decoration: none;font-weight: bold;display: block;text-align: center;background-color: #0073b2!important;", :target => "_blank"} + %a.soc-btn.li{:href => "http://www.linkedin.com/groups/Open-Food-Foundation-4743336", :target => "_blank"} LinkedIn - / /column 1 - / column 2 - %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %h5{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 900;font-size: 17px;"} + %table.column{:align => "left"} + %tr + %td + %h5 Email us: - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %a{:href => "mailto:hello@openfoodnetwork.org", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;"} + %p + %strong + %a{:href => "mailto:hello@openfoodnetwork.org"} hello@openfoodnetwork.org - / /column 2 - %span.clear{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block;clear: both;"} + %span.clear diff --git a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml index 50816370e5..0de5b177d8 100644 --- a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml @@ -1,109 +1,79 @@ -%h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} - = "Hi #{@order.bill_address.firstname}," -%table.social.white-bg{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;background-color: white!important;width: 100%;border: 1px solid #ebebeb;", :width => "100%"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - / column 1 - %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - = "Order confirmation ##{@order.number}" - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +%h3 Hi #{@order.bill_address.firstname}, +%table.social.white-bg{:width => "100%"} + %tr + %td + %table.column{:align => "left"} + %tr + %td + %p + %strong Order confirmation ##{@order.number} + %p Thanks for shopping on %strong= "#{Spree::Config.site_name}." Here are your order details from %strong= "#{@order.distributor.name}." - / /column 1 - / column 2 - %table.column{:align => "left", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 280px;float: left;min-width: 279px;"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %img.float-right{:src => "#{@order.distributor.logo.url(:thumb)}", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;max-width: 100%;float: right;display: block;"}/ - / /column 2 - %span.clear{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;display: block;clear: both;"} -/ /intro + %table.column{:align => "left"} + %tr + %td + %img.float-right{:src => "#{@order.distributor.logo.url(:thumb)}"}/ + %span.clear +%p   -/ Heading Panel -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   -%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - Order summary -/ /Heading Panel -/ Order Summary -/

    Order for: Rob Harrington

    -%table.order-summary{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;", :width => "100%"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - Item - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :width => "25%"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - Qty - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;", :width => "25%"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - Price +%p.callout + %strong Order summary +%table.order-summary{:width => "100%"} + %tr + %td + %strong Item + %td{:align => "right", :width => "25%"} + %strong Qty + %td{:align => "right", :width => "25%"} + %strong Price - @order.line_items.each do |item| - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %tr + %td = raw(item.variant.product.supplier.name) %br = "#{raw(item.variant.product.name)} #{raw(item.variant.options_text)}" - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right"} = item.quantity - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right"} = item.display_amount - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:colspan => "3", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} -   - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:align => "right", :colspan => "2", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %tr + %td{:colspan => "3"}   + %tr + %td{:align => "right", :colspan => "2"} Subtotal: - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right"} = spree_number_to_currency checkout_cart_total_with_adjustments(@order) - checkout_adjustments_for_summary(@order, exclude: [:distribution]).reject{ |a| a.amount == 0 }.each do |adjustment| - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:align => "right", :colspan => "2", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %tr + %td{:align => "right", :colspan => "2"} = raw(adjustment.label) - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %td{:align => "right"} = adjustment.display_amount - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:align => "right", :colspan => "2", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - Total: - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - = @order.display_total + %tr + %td{:align => "right", :colspan => "2"} + %strong Total: + %td{:align => "right"} + %strong= @order.display_total +%p   -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   - --# Payment Summary - if @order.payments.first.andand.payment_method.andand.type == "Spree::PaymentMethod::Check" and @order.payments.first.andand.payment_method.andand.description - / Heading Panel - %p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - Payment summary + %p.callout + %strong Payment summary / /Heading Panel / Payment Summary - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - = @order.payments.first.andand.payment_method.andand.description.andand.html_safe - - -# Spacer - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   - - + %p= @order.payments.first.andand.payment_method.andand.description.andand.html_safe + %p   - if @order.shipping_method.andand.require_ship_address - -# Delivery details - %p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + / Delivery details + %p.callout + %strong Delivery details - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + %p Your order will be delivered to: %br #{@order.ship_address.to_s} @@ -123,11 +93,11 @@ - else - -# Collection Details - %p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + / Collection details + %p.callout + %strong Collection details - %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} + %p - if @order.shipping_method.andand.description = @order.shipping_method.description.html_safe %br @@ -152,27 +122,15 @@ -# %a{:href => "https://goo.gl/maps/T1ArU", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} -# google maps -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   -%hr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - Thanks for your support. -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} -   --# Contact details -%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} +%p.callout #{@order.distributor.contact}, - %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - = @order.distributor.name - %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ - %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ + %br/ + %strong= @order.distributor.name + %br/ = @order.distributor.phone || "" - %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ - %a{:href => "mailto:#{@order.distributor.email}", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;font-weight: bold;", :target => "_blank"} + %br/ + %a{:href => "mailto:#{@order.distributor.email}", :target => "_blank"} = @order.distributor.email - %br{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ + %br/ = @order.distributor.website || "" \ No newline at end of file diff --git a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml index 8f40e0a148..0de5b177d8 100644 --- a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml @@ -22,9 +22,6 @@ %p.callout %strong Order summary -/ /Heading Panel -/ Order Summary -/

    Order for: Rob Harrington

    %table.order-summary{:width => "100%"} %tr %td From f5a5b93c2238036ef5b287250c802a0efc0860ea Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 15:45:12 +1100 Subject: [PATCH 233/681] Can use relative paths for assets in emails because roadie --- app/helpers/spree/mailer_helper.rb | 7 ------- app/mailers/spree/base_mailer_decorator.rb | 6 ++++++ app/views/layouts/mailer.html.haml | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 app/helpers/spree/mailer_helper.rb diff --git a/app/helpers/spree/mailer_helper.rb b/app/helpers/spree/mailer_helper.rb deleted file mode 100644 index f0e4a777a1..0000000000 --- a/app/helpers/spree/mailer_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Spree - module MailerHelper - def email_asset_url(asset) - URI.join(root_url, asset_path(asset)).to_s - end - end -end \ No newline at end of file diff --git a/app/mailers/spree/base_mailer_decorator.rb b/app/mailers/spree/base_mailer_decorator.rb index a776eb8946..e02751e574 100644 --- a/app/mailers/spree/base_mailer_decorator.rb +++ b/app/mailers/spree/base_mailer_decorator.rb @@ -5,4 +5,10 @@ Spree::BaseMailer.class_eval do # Define layout layout 'mailer' helper Spree::MailerHelper + + protected + def roadie_options + binding.pry + super.merge(url_options: {host: URI(spree.root_url).host }) + end end \ No newline at end of file diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 1f3dbdb2cf..554c80a320 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -15,7 +15,7 @@ %table{:bgcolor => "#333333"} %tr %td - %img{:src => "#{ email_asset_url 'open-food-network-beta.png' }", srcset: "#{ email_asset_url 'open-food-network-beta.svg' }", :width => "200"}/ + %img{:src => "#{ asset_path 'open-food-network-beta.png' }", srcset: "#{ asset_path 'open-food-network-beta.svg' }", :width => "200"}/ %td{:align => "right"} %h6.collapse Open Food Network From 1f42d321512513e2c0281a371bcfd0062e001d76 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 15:45:50 +1100 Subject: [PATCH 234/681] Remove naughty pry --- app/mailers/spree/base_mailer_decorator.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/mailers/spree/base_mailer_decorator.rb b/app/mailers/spree/base_mailer_decorator.rb index e02751e574..ea0808d7c8 100644 --- a/app/mailers/spree/base_mailer_decorator.rb +++ b/app/mailers/spree/base_mailer_decorator.rb @@ -8,7 +8,6 @@ Spree::BaseMailer.class_eval do protected def roadie_options - binding.pry super.merge(url_options: {host: URI(spree.root_url).host }) end end \ No newline at end of file From 4dd5e51ed51a7e3a6a16a0f773bd950d66690064 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 15:48:36 +1100 Subject: [PATCH 235/681] Stripping out inline styles from user signup email template --- .../user_mailer/signup_confirmation.html.haml | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/views/spree/user_mailer/signup_confirmation.html.haml b/app/views/spree/user_mailer/signup_confirmation.html.haml index 41ead24c2b..7e68f9fd77 100644 --- a/app/views/spree/user_mailer/signup_confirmation.html.haml +++ b/app/views/spree/user_mailer/signup_confirmation.html.haml @@ -1,32 +1,32 @@ -%h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} +%h3 Hello! -%p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} +%p.lead = "Welcome to #{ Spree::Config[:site_name] }!" / Heading Panel %p   -%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} +%p.callout + %strong Your login -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +%p Your login email is - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %strong = @user.email -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +%p You can start shopping online now at - %a{:href => "#{ spree.root_url }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + %a{:href => "#{ spree.root_url }", :target => "_blank"} -# Remove http:// and trailing slashes from root url if they exist = spree.root_url.sub(/http:\/\//,"").sub(/\/$/,"") %p   -%hr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"}/ +%hr/ %p   -%p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} +%p.lead Thanks for joining the network. We look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +%p We welcome all your questions and feedback; you can use the - %em{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %em Send Feedback button on the site or email us at - %a{:href => "mailto:hello@openfoodnetwork.org", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + %a{:href => "mailto:hello@openfoodnetwork.org", :target => "_blank"} hello@openfoodnetwork.org = render 'shared/mailers/signoff' From 3d98ec6eef6bdb286cf6811bae827a52361da850 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 15:51:28 +1100 Subject: [PATCH 236/681] Stripping out inline styles from enterprise email templates --- .../confirmation_instructions.html.haml | 12 ++-- app/views/enterprise_mailer/welcome.html.haml | 56 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app/views/enterprise_mailer/confirmation_instructions.html.haml b/app/views/enterprise_mailer/confirmation_instructions.html.haml index 3ea65a2048..e957b70b1c 100644 --- a/app/views/enterprise_mailer/confirmation_instructions.html.haml +++ b/app/views/enterprise_mailer/confirmation_instructions.html.haml @@ -1,19 +1,19 @@ -%h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} +%h3 = "Hi, #{@resource.contact}!" -%p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} +%p.lead = "Please confirm your email address for " %strong = "#{@resource.name}." %p   -/ Callout Panel -%p.callout{:style => "margin: 0; padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} + +%p.callout Click the link below to confirm your email and to activate your enterprise. This link can be used only once: %br %strong = link_to 'Confirm this email address »', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) -/ /Callout Panel + %p   -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +%p = "We're so excited that you're joining the #{ Spree::Config[:site_name] }! Don't hestitate to get in touch if you have any questions." = render 'shared/mailers/signoff' diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index 8d3dc13587..4f2f2e6fc8 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -1,62 +1,62 @@ -%h3{:style => "margin: 0;padding: 0;font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;line-height: 1.1;margin-bottom: 15px;color: #000;font-weight: 500;font-size: 27px;"} +%h3 = "Welcome, #{@enterprise.contact}!" -%p.lead{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 17px;line-height: 1.6;"} +%p.lead Congratulations, - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %strong %strong= @enterprise.name = "is now part of #{ Spree::Config.site_name }!" / Heading Panel -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +%p Please find below all the details for viewing and editing your enterprise on %strong= "#{ Spree::Config.site_name }." We suggest keeping this email and information somewhere safe. Logging in with the account details below will allow complete access to your products and services. %p   -%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} +%p.callout + %strong Your enterprise details -%table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;", :width => "100%"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} +%table{:width => "100%"} + %tr + %td{:align => "right"} + %strong Shop URL %td   - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %a{:href => "#{ main_app.shop_enterprise_url(@enterprise) }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + %td + %a{:href => "#{ main_app.shop_enterprise_url(@enterprise) }", :target => "_blank"} = main_app.shop_enterprise_url(@enterprise) - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %tr %td   - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} + %tr + %td{:align => "right"} + %strong Email %td   - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %a{:href => "mailto: #{ @enterprise.email }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + %td + %a{:href => "mailto: #{ @enterprise.email }", :target => "_blank"} = @enterprise.email %p   -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +%p Log into %strong= "#{ Spree::Config.site_name } Admin" in order to edit your enterprise details such as website and social media links, or to start adding products to your enterprise! -%p.callout{:style => "margin: 0;padding: 15px;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 15px;font-weight: normal;font-size: 14px;line-height: 1.6;background-color: #e1f0f5;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} +%p.callout + %strong OFN Admin -%table{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;width: 100%;", :width => "100%"} - %tr{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %td{:align => "right", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} +%table{ :width => "100%"} + %tr + %td{:align => "right"} + %strong Admin %td   - %td{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - %a{:href => "#{ spree.admin_url }", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} + %td + %a{:href => "#{ spree.admin_url }", :target => "_blank"} = spree.admin_url %p   / /Heading Panel -%p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} +%p We're so pleased to have you as a valued member of %strong= "#{Spree::Config.site_name}!" Don't hestitate to get in touch if you have any questions. From 76c300283b8962ddf7d07497b2c032537efb8c52 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 20 Nov 2014 15:59:06 +1100 Subject: [PATCH 237/681] Remove CONTACT_STRING config - Rob has an admin-managable override for this coming soon --- app/views/spree/user_mailer/signup_confirmation.text.haml | 3 +-- config/application.yml.example | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/views/spree/user_mailer/signup_confirmation.text.haml b/app/views/spree/user_mailer/signup_confirmation.text.haml index 3a30b6827e..218e90e8bc 100644 --- a/app/views/spree/user_mailer/signup_confirmation.text.haml +++ b/app/views/spree/user_mailer/signup_confirmation.text.haml @@ -9,5 +9,4 @@ We welcome all your questions and feedback; you can use the Send Feedback button Thanks for getting on board and we look forward to introducing you to many more great farmers, food hubs and food! Cheers, -#{ENV['CONTACT_STRING']} - +Kirsten Larsen and the OFN Team diff --git a/config/application.yml.example b/config/application.yml.example index 986e1c9e22..6e11a75dca 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -11,6 +11,3 @@ DEFAULT_COUNTRY: "Australia" I18N_LOCALE: "en" # Spree zone. CHECKOUT_ZONE: "Australia" -# Contact name for emails. -CONTACT_STRING: "Joe Bloggs and the OFN Team" - From b941ffabeb7b12b70ee726029d694adb957221dd Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 20 Nov 2014 16:20:07 +1100 Subject: [PATCH 238/681] Update README for Figaro config, add Maikel to credits --- README.markdown | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index 2710aa1162..8c9883dc04 100644 --- a/README.markdown +++ b/README.markdown @@ -48,6 +48,11 @@ Install the project's gem dependencies: bundle install +Configure the site: + + cp config/application.yml.example config/application.yml + edit config/application.yml + Create the development and test databases, using the settings specified in `config/database.yml`: rake db:setup @@ -56,7 +61,7 @@ Then load the schema and some seed data with the following command: rake db:schema:load db:seed -Load some default data for your environment +Load some default data for your environment: rake openfoodnetwork:dev:load_sample_data @@ -69,7 +74,7 @@ At long last, your dreams of spinning up a development server can be realised: Tests, both unit and integration, are based on RSpec. To run the test suite, first prepare the test database: - bundle exec rake db:test:load + bundle exec rake db:test:prepare Then the tests can be run with: @@ -90,7 +95,7 @@ usage instructions. * David Cook (http://github.com/dacook) * Will Marshall (http://soundcloud.com/willmarshall) * Laura Summers (https://github.com/summerscope) - +* Maikel Linke (https://github.com/mkllnk) ## Licence From 46462a1abb60cb6d8a62084c3ef5c650c2872dbb Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 20:13:54 +1100 Subject: [PATCH 239/681] Format fiddling in user signup confirm email --- .../spree/user_mailer/signup_confirmation.html.haml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/spree/user_mailer/signup_confirmation.html.haml b/app/views/spree/user_mailer/signup_confirmation.html.haml index 7e68f9fd77..fb52f57119 100644 --- a/app/views/spree/user_mailer/signup_confirmation.html.haml +++ b/app/views/spree/user_mailer/signup_confirmation.html.haml @@ -1,5 +1,5 @@ %h3 -Hello! + Hello! %p.lead = "Welcome to #{ Spree::Config[:site_name] }!" @@ -25,9 +25,10 @@ Hello! %p We welcome all your questions and feedback; you can use the %em - Send Feedback button on the site or email us at - %a{:href => "mailto:hello@openfoodnetwork.org", :target => "_blank"} - hello@openfoodnetwork.org + Send Feedback + button on the site or email us at + %a{:href => "mailto:hello@openfoodnetwork.org", :target => "_blank"} + hello@openfoodnetwork.org = render 'shared/mailers/signoff' From c310a3bdaa8dd24265367656bf19ca2e54d550f3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 20:14:57 +1100 Subject: [PATCH 240/681] Don't use svg for email header --- app/views/layouts/mailer.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 554c80a320..ef8d4e2ec1 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -15,7 +15,7 @@ %table{:bgcolor => "#333333"} %tr %td - %img{:src => "#{ asset_path 'open-food-network-beta.png' }", srcset: "#{ asset_path 'open-food-network-beta.svg' }", :width => "200"}/ + %img{:src => "#{ asset_path 'open-food-network-beta.png' }", :width => "200"}/ %td{:align => "right"} %h6.collapse Open Food Network From b64b48f26ec149125741fc19e5e30c79d8ab79b2 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 20:16:34 +1100 Subject: [PATCH 241/681] Removing obsolete MailerHelper reference --- app/mailers/spree/base_mailer_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/mailers/spree/base_mailer_decorator.rb b/app/mailers/spree/base_mailer_decorator.rb index ea0808d7c8..4f78c1fe1e 100644 --- a/app/mailers/spree/base_mailer_decorator.rb +++ b/app/mailers/spree/base_mailer_decorator.rb @@ -4,10 +4,10 @@ Spree::BaseMailer.class_eval do # Define layout layout 'mailer' - helper Spree::MailerHelper protected def roadie_options + # This lets us specify assets using relative paths in email templates super.merge(url_options: {host: URI(spree.root_url).host }) end end \ No newline at end of file From ebaaf2a136c2c89397eddb53914a83bc036af46f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 20 Nov 2014 20:17:18 +1100 Subject: [PATCH 242/681] Display line items in confirmation email with fees included in item totals --- app/models/spree/line_item_decorator.rb | 10 ++++++++++ .../order_mailer/confirm_email_for_customer.html.haml | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index ba12d366be..9ce49eb67e 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -22,4 +22,14 @@ Spree::LineItem.class_eval do joins(:product). where('spree_products.supplier_id IN (?)', enterprises) } + + def amount_with_adjustments + # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, + # so line_item.adjustments returns an empty array + amount + Spree::Adjustment.where(source_id: id).sum(&:amount) + end + + def display_amount_with_adjustments + Spree::Money.new(amount_with_adjustments, { :currency => currency }) + end end diff --git a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml index 0de5b177d8..65b7a202d8 100644 --- a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml @@ -10,7 +10,7 @@ %p Thanks for shopping on %strong= "#{Spree::Config.site_name}." - Here are your order details from + Here are the details for you order from %strong= "#{@order.distributor.name}." %table.column{:align => "left"} %tr @@ -39,7 +39,8 @@ %td{:align => "right"} = item.quantity %td{:align => "right"} - = item.display_amount + = item.display_amount_with_adjustments + %tr %td{:colspan => "3"}   %tr From 021cca4fca688b58757ca85b7768859a9128bb96 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 2 Sep 2014 12:06:35 +1000 Subject: [PATCH 243/681] Stylistic changes --- app/models/spree/product_decorator.rb | 2 +- spec/models/spree/product_spec.rb | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index d344e19935..5181bf776d 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -185,7 +185,7 @@ Spree::Product.class_eval do if variant_unit_changed? option_types.delete self.class.all_variant_unit_option_types option_types << variant_unit_option_type if variant_unit.present? - variants_including_master.each { |v| v.update_units } + variants_including_master.each &:update_units end end diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index f82d07674f..c6acae0850 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -27,12 +27,6 @@ module Spree product.should_not be_valid end - it "defaults available_on to now" do - Timecop.freeze - product = Product.new - product.available_on.should == Time.now - end - it "does not save when master is invalid" do s = create(:supplier_enterprise) t = create(:taxon) @@ -41,6 +35,12 @@ module Spree product.save.should be_false end + it "defaults available_on to now" do + Timecop.freeze + product = Product.new + product.available_on.should == Time.now + end + context "when the product has variants" do let(:product) do product = create(:simple_product) From 29e49b67cce20b0d8c6b1dcfa5861679b34ae2a0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Sep 2014 13:22:50 +1000 Subject: [PATCH 244/681] In BPE, rename producer and category JSON attrs to producer_id and category_id Conflicts: spec/features/admin/bulk_product_update_spec.rb --- .../admin/bulk_product_update.js.coffee | 8 +++---- .../admin/filters/category_filter.js.coffee | 2 +- .../admin/filters/producer_filter.js.coffee | 2 +- .../spree/api/product_serializer.rb | 16 ++++++------- .../bulk_edit/_products_product.html.haml | 4 ++-- .../admin/bulk_product_update_spec.rb | 24 +++++++++---------- .../unit/bulk_product_update_spec.js.coffee | 10 ++++---- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index c95f27932e..417ace3d6b 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -386,8 +386,8 @@ filterSubmitProducts = (productsToFilter) -> if product.hasOwnProperty("name") filteredProduct.name = product.name hasUpdatableProperty = true - if product.hasOwnProperty("producer") - filteredProduct.supplier_id = product.producer + if product.hasOwnProperty("producer_id") + filteredProduct.supplier_id = product.producer_id hasUpdatableProperty = true if product.hasOwnProperty("price") filteredProduct.price = product.price @@ -402,8 +402,8 @@ filterSubmitProducts = (productsToFilter) -> if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present filteredProduct.on_hand = product.on_hand hasUpdatableProperty = true - if product.hasOwnProperty("category") - filteredProduct.primary_taxon_id = product.category + if product.hasOwnProperty("category_id") + filteredProduct.primary_taxon_id = product.category_id hasUpdatableProperty = true if product.hasOwnProperty("available_on") filteredProduct.available_on = product.available_on diff --git a/app/assets/javascripts/admin/filters/category_filter.js.coffee b/app/assets/javascripts/admin/filters/category_filter.js.coffee index b89e706815..8472a0f7dc 100644 --- a/app/assets/javascripts/admin/filters/category_filter.js.coffee +++ b/app/assets/javascripts/admin/filters/category_filter.js.coffee @@ -1,4 +1,4 @@ angular.module("ofn.admin").filter "category", ($filter) -> return (products, taxonID) -> return products if taxonID == "0" - return $filter('filter')( products, { category: taxonID }, true ) \ No newline at end of file + return $filter('filter')( products, { category_id: taxonID }, true ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/filters/producer_filter.js.coffee b/app/assets/javascripts/admin/filters/producer_filter.js.coffee index 7325b2200d..bda3a9c751 100644 --- a/app/assets/javascripts/admin/filters/producer_filter.js.coffee +++ b/app/assets/javascripts/admin/filters/producer_filter.js.coffee @@ -1,4 +1,4 @@ angular.module("ofn.admin").filter "producer", ($filter) -> return (products, producerID) -> return products if producerID == "0" - $filter('filter')( products, { producer: producerID }, true ) \ No newline at end of file + $filter('filter')( products, { producer_id: producerID }, true ) \ No newline at end of file diff --git a/app/serializers/spree/api/product_serializer.rb b/app/serializers/spree/api/product_serializer.rb index aa8919f8d6..8c1a331936 100644 --- a/app/serializers/spree/api/product_serializer.rb +++ b/app/serializers/spree/api/product_serializer.rb @@ -2,25 +2,25 @@ class Spree::Api::ProductSerializer < ActiveModel::Serializer attributes :id, :name, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand attributes :on_hand, :price, :available_on, :permalink_live - - has_one :supplier, key: :producer, embed: :id - has_one :primary_taxon, key: :category, embed: :id + + has_one :supplier, key: :producer_id, embed: :id + has_one :primary_taxon, key: :category_id, embed: :id has_many :variants, key: :variants, serializer: Spree::Api::VariantSerializer # embed: ids has_one :master, serializer: Spree::Api::VariantSerializer - + def on_hand object.on_hand.nil? ? 0 : object.on_hand.to_f.finite? ? object.on_hand : "On demand" end - + def price object.price.nil? ? '0.0' : object.price end - + def available_on object.available_on.blank? ? "" : object.available_on.strftime("%F %T") end - + def permalink_live object.permalink end -end \ No newline at end of file +end diff --git a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml index ee2c1dc136..ed41532a11 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml @@ -3,7 +3,7 @@ %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" } %td.producer{ 'ng-show' => 'columns.producer.visible' } - %select.select2.fullwidth{ 'ng-model' => 'product.producer', :name => 'producer', 'ofn-track-product' => 'producer', 'ng-options' => 'producer.id as producer.name for producer in producers' } + %select.select2.fullwidth{ 'ng-model' => 'product.producer_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' } %td.name{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' } %td.unit{ 'ng-show' => 'columns.unit.visible' } @@ -19,7 +19,7 @@ %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.category{ 'ng-if' => 'columns.category.visible' } - %input.fullwidth{ :type => 'text', id: "p{{product.id}}_category", 'ng-model' => 'product.category', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category' } + %input.fullwidth{ :type => 'text', id: "p{{product.id}}_category_id", 'ng-model' => 'product.category_id', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category_id' } %td.available_on{ 'ng-show' => 'columns.available_on.visible' } %input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ofn-track-product' => 'available_on', 'datetimepicker' => 'product.available_on', type: "text" } %td.actions diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 03a90137d9..65776c4845 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -37,8 +37,8 @@ feature %q{ visit '/admin/products/bulk_edit' - expect(page).to have_select "producer", with_options: [s1.name,s2.name,s3.name], selected: s2.name - expect(page).to have_select "producer", with_options: [s1.name,s2.name,s3.name], selected: s3.name + expect(page).to have_select "producer_id", with_options: [s1.name,s2.name,s3.name], selected: s2.name + expect(page).to have_select "producer_id", with_options: [s1.name,s2.name,s3.name], selected: s3.name end it "displays a date input for available_on for each product, formatted to yyyy-mm-dd hh:mm:ss" do @@ -302,19 +302,19 @@ feature %q{ within "tr#p_#{p.id}" do expect(page).to have_field "product_name", with: p.name - expect(page).to have_select "producer", selected: s1.name + expect(page).to have_select "producer_id", selected: s1.name expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") expect(page).to have_field "price", with: "10.0" - expect(page).to have_selector "div#s2id_p#{p.id}_category a.select2-choice" + expect(page).to have_selector "div#s2id_p#{p.id}_category_id a.select2-choice" expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)" expect(page).to have_field "on_hand", with: "6" fill_in "product_name", with: "Big Bag Of Potatoes" - select s2.name, :from => 'producer' + select s2.name, :from => 'producer_id' fill_in "available_on", with: (3.days.ago.beginning_of_day).strftime("%F %T") fill_in "price", with: "20" select "Weight (kg)", from: "variant_unit_with_scale" - select2_select t1.name, from: "p#{p.id}_category" + select2_select t1.name, from: "p#{p.id}_category_id" fill_in "on_hand", with: "18" fill_in "display_as", with: "Big Bag" end @@ -654,13 +654,13 @@ feature %q{ end expect(page).to have_selector "a.clone-product", :count => 4 expect(page).to have_field "product_name", with: "COPY OF #{p1.name}" - expect(page).to have_select "producer", selected: "#{p1.supplier.name}" + expect(page).to have_select "producer_id", selected: "#{p1.supplier.name}" visit '/admin/products/bulk_edit' expect(page).to have_selector "a.clone-product", :count => 4 expect(page).to have_field "product_name", with: "COPY OF #{p1.name}" - expect(page).to have_select "producer", selected: "#{p1.supplier.name}" + expect(page).to have_select "producer_id", selected: "#{p1.supplier.name}" end end end @@ -764,8 +764,8 @@ feature %q{ it "shows only suppliers that I manage or have permission to" do visit '/admin/products/bulk_edit' - expect(page).to have_select 'producer', with_options: [supplier_managed1.name, supplier_managed2.name, supplier_permitted.name], selected: supplier_managed1.name - expect(page).to have_no_select 'producer', with_options: [supplier_unmanaged.name] + expect(page).to have_select 'producer_id', with_options: [supplier_managed1.name, supplier_managed2.name, supplier_permitted.name], selected: supplier_managed1.name + expect(page).to have_no_select 'producer_id', with_options: [supplier_unmanaged.name] end it "shows inactive products that I supply" do @@ -807,13 +807,13 @@ feature %q{ within "tr#p_#{p.id}" do expect(page).to have_field "product_name", with: p.name - expect(page).to have_select "producer", selected: supplier_permitted.name + expect(page).to have_select "producer_id", selected: supplier_permitted.name expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") expect(page).to have_field "price", with: "10.0" expect(page).to have_field "on_hand", with: "6" fill_in "product_name", with: "Big Bag Of Potatoes" - select(supplier_managed2.name, :from => 'producer') + select supplier_managed2.name, :from => 'producer_id' fill_in "available_on", with: (3.days.ago.beginning_of_day).strftime("%F %T") fill_in "price", with: "20" select "Weight (kg)", from: "variant_unit_with_scale" diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 649ab99f52..fa7ebdd82d 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -184,7 +184,7 @@ describe "filtering products for submission to database", -> created_at: null updated_at: null count_on_hand: 0 - producer: 5 + producer_id: 5 group_buy: null group_buy_unit_size: null @@ -1051,7 +1051,7 @@ describe "AdminProductEditCtrl", -> product: id: 17 name: "new_product" - producer: 6 + producer_id: 6 variants: [ id: 3 @@ -1061,7 +1061,7 @@ describe "AdminProductEditCtrl", -> $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, id: 17 name: "new_product" - producer: 6 + producer_id: 6 variants: [ id: 3 @@ -1074,7 +1074,7 @@ describe "AdminProductEditCtrl", -> id: 17 name: "new_product" variant_unit_with_scale: null - producer: 6 + producer_id: 6 variants: [ id: 3 @@ -1091,7 +1091,7 @@ describe "AdminProductEditCtrl", -> id: 17 name: "new_product" variant_unit_with_scale: null - producer: 6 + producer_id: 6 variants: [ id: 3 From f117c0cd249700ee55089124bfe54bdc76487afb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Sep 2014 13:39:03 +1000 Subject: [PATCH 245/681] Make spree_current_user available to serializers as 'scope' --- app/helpers/admin/injection_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 094220b5a4..7db604ba5f 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -49,12 +49,12 @@ module Admin def admin_inject_json_ams(ngModule, name, data, serializer, opts = {}) - json = serializer.new(data).to_json + json = serializer.new(data, scope: spree_current_user).to_json render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} end def admin_inject_json_ams_array(ngModule, name, data, serializer, opts = {}) - json = ActiveModel::ArraySerializer.new(data, {each_serializer: serializer}.merge(opts)).to_json + json = ActiveModel::ArraySerializer.new(data, {each_serializer: serializer, scope: spree_current_user}.merge(opts)).to_json render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} end end From 356a03ac4eea4e57162b90256f0c4f24239d3c57 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Sep 2014 17:06:17 +1000 Subject: [PATCH 246/681] WIP: Extract bulk product edit product management to BulkProducts service Conflicts: app/assets/javascripts/admin/bulk_product_update.js.coffee --- .../admin/bulk_product_update.js.coffee | 97 +++---------------- .../admin/services/bulk_products.js.coffee | 80 +++++++++++++++ 2 files changed, 93 insertions(+), 84 deletions(-) create mode 100644 app/assets/javascripts/admin/services/bulk_products.js.coffee diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 417ace3d6b..9694ff761e 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,6 +1,4 @@ -angular.module("ofn.admin").controller "AdminProductEditCtrl", [ - "$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", "producers", "Taxons", "SpreeApiKey", - ($scope, $timeout, $http, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons, SpreeApiKey) -> +angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons, SpreeApiKey) -> $scope.loading = true $scope.updateStatusMessage = @@ -38,12 +36,13 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons $scope.producerFilter = "0" $scope.categoryFilter = "0" - $scope.products = [] + $scope.products = BulkProducts.products $scope.filteredProducts = [] $scope.currentFilters = [] $scope.limit = 15 $scope.productsWithUnsavedVariants = [] $scope.query = "" + $scope.displayProperties = {} $scope.initialise = -> authorise_api_reponse = "" @@ -61,36 +60,18 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.$watchCollection '[query, producerFilter, categoryFilter]', -> $scope.limit = 15 # Reset limit whenever searching - $scope.fetchProducts = -> # WARNING: returns a promise + $scope.fetchProducts = -> $scope.loading = true - queryString = $scope.currentFilters.reduce (qs,f) -> - return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};" - , "" - return dataFetcher("/api/products/bulk_products?page=1;per_page=20;#{queryString}").then (data) -> - $scope.resetProducts data.products + BulkProducts.fetch($scope.currentFilters).then -> + $scope.resetProducts() $scope.loading = false - if data.pages > 1 - for page in [2..data.pages] - dataFetcher("/api/products/bulk_products?page=#{page};per_page=20;#{queryString}").then (data) -> - for product in data.products - $scope.unpackProduct product - $scope.products.push product - $scope.resetProducts = (data) -> - $scope.products = data + $scope.resetProducts = -> DirtyProducts.clear() $scope.setMessage $scope.updateStatusMessage, "", {}, false - $scope.displayProperties ||= {} - angular.forEach $scope.products, (product) -> - $scope.unpackProduct product - - - $scope.unpackProduct = (product) -> - $scope.displayProperties ||= {} - $scope.displayProperties[product.id] ||= showVariants: false - #$scope.matchProducer product - $scope.loadVariantUnit product + for product in $scope.products + $scope.displayProperties[product.id] ||= showVariants: false # $scope.matchProducer = (product) -> @@ -99,37 +80,6 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ # product.producer = producer # break - $scope.loadVariantUnit = (product) -> - product.variant_unit_with_scale = - if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items' - "#{product.variant_unit}_#{product.variant_unit_scale}" - else if product.variant_unit - product.variant_unit - else - null - - $scope.loadVariantUnitValues product if product.variants - $scope.loadVariantUnitValue product, product.master if product.master - - $scope.loadVariantUnitValues = (product) -> - for variant in product.variants - $scope.loadVariantUnitValue product, variant - - $scope.loadVariantUnitValue = (product, variant) -> - unit_value = $scope.variantUnitValue product, variant - unit_value = if unit_value? then unit_value else '' - variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim() - - - $scope.variantUnitValue = (product, variant) -> - if variant.unit_value? - if product.variant_unit_scale - variant.unit_value / product.variant_unit_scale - else - variant.unit_value - else - null - $scope.updateOnHand = (product) -> on_demand_variants = [] @@ -183,12 +133,6 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.variantIdCounter -= 1 $scope.variantIdCounter - $scope.updateVariantLists = (server_products) -> - for product in $scope.productsWithUnsavedVariants - server_product = $scope.findProduct(product.id, server_products) - product.variants = server_product.variants - $scope.loadVariantUnitValues product - $scope.deleteProduct = (product) -> if confirm("Are you sure?") $http( @@ -218,18 +162,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.cloneProduct = (product) -> - dataFetcher("/admin/products/" + product.permalink_live + "/clone.json").then (data) -> - # Ideally we would use Spree's built in respond_override helper here to redirect the - # user after a successful clone with .json in the accept headers - # However, at the time of writing there appears to be an issue which causes the - # respond_with block in the destroy action of Spree::Admin::Product to break - # when a respond_overrride for the clone action is used. - id = data.product.id - dataFetcher("/api/products/" + id + "?template=bulk_show").then (data) -> - newProduct = data - $scope.unpackProduct newProduct - $scope.products.push newProduct - + BulkProducts.cloneProduct product $scope.hasVariants = (product) -> product.variants.length > 0 @@ -270,7 +203,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ filters: $scope.currentFilters ).success((data) -> DirtyProducts.clear() - $scope.updateVariantLists(data.products) + BulkProducts.updateVariantLists(data.products, $scope.productsWithUnsavedVariants) $timeout -> $scope.displaySuccess() ).error (data, status) -> if status == 400 && data.errors? && data.errors.length > 0 @@ -304,16 +237,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ if variant.hasOwnProperty("unit_value_with_description") match = variant.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/) if match - product = $scope.findProduct(product.id, $scope.products) + product = BulkProducts.find product.id variant.unit_value = parseFloat(match[1]) variant.unit_value = null if isNaN(variant.unit_value) variant.unit_value *= product.variant_unit_scale if variant.unit_value && product.variant_unit_scale variant.unit_description = match[3] - $scope.findProduct = (id, product_list) -> - products = (product for product in product_list when product.id == id) - if products.length == 0 then null else products[0] - $scope.incrementLimit = -> if $scope.limit < $scope.products.length $scope.limit = $scope.limit + 5 @@ -354,7 +283,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ , false else $scope.setMessage $scope.updateStatusMessage, "", {}, false -] + filterSubmitProducts = (productsToFilter) -> filteredProducts = [] diff --git a/app/assets/javascripts/admin/services/bulk_products.js.coffee b/app/assets/javascripts/admin/services/bulk_products.js.coffee new file mode 100644 index 0000000000..dd2e6536af --- /dev/null +++ b/app/assets/javascripts/admin/services/bulk_products.js.coffee @@ -0,0 +1,80 @@ +angular.module("ofn.admin").factory "BulkProducts", (dataFetcher) -> + new class BulkProducts + products: [] + + fetch: (filters, onComplete) -> + queryString = filters.reduce (qs,f) -> + return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};" + , "" + return dataFetcher("/api/products/bulk_products?page=1;per_page=20;#{queryString}").then (data) => + @addProducts data.products + + if data.pages > 1 + for page in [2..data.pages] + dataFetcher("/api/products/bulk_products?page=#{page};per_page=20;#{queryString}").then (data) => + @addProducts data.products + + cloneProduct: (product) -> + dataFetcher("/admin/products/" + product.permalink_live + "/clone.json").then (data) => + # Ideally we would use Spree's built in respond_override helper here to redirect the + # user after a successful clone with .json in the accept headers + # However, at the time of writing there appears to be an issue which causes the + # respond_with block in the destroy action of Spree::Admin::Product to break + # when a respond_overrride for the clone action is used. + id = data.product.id + dataFetcher("/api/products/" + id + "?template=bulk_show").then (data) => + newProduct = data + @unpackProduct newProduct + @products.push newProduct + + updateVariantLists: (serverProducts, productsWithUnsavedVariants) -> + for product in productsWithUnsavedVariants + server_product = @findProductInList(product.id, serverProducts) + product.variants = server_product.variants + @loadVariantUnitValues product + + find: (id) -> + @findProductInList id, @products + + findProductInList: (id, product_list) -> + products = (product for product in product_list when product.id == id) + if products.length == 0 then null else products[0] + + addProducts: (products) -> + for product in products + @unpackProduct product + @products.push product + + unpackProduct: (product) -> + #$scope.matchProducer product + @loadVariantUnit product + + loadVariantUnit: (product) -> + product.variant_unit_with_scale = + if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items' + "#{product.variant_unit}_#{product.variant_unit_scale}" + else if product.variant_unit + product.variant_unit + else + null + + @loadVariantUnitValues product if product.variants + @loadVariantUnitValue product, product.master if product.master + + loadVariantUnitValues: (product) -> + for variant in product.variants + @loadVariantUnitValue product, variant + + loadVariantUnitValue: (product, variant) -> + unit_value = @variantUnitValue product, variant + unit_value = if unit_value? then unit_value else '' + variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim() + + variantUnitValue: (product, variant) -> + if variant.unit_value? + if product.variant_unit_scale + variant.unit_value / product.variant_unit_scale + else + variant.unit_value + else + null From 7e4d33777333d83a7bb24c2eac0d4fe322039355 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Sep 2014 16:42:20 +1000 Subject: [PATCH 247/681] Extract DisplayProperties to a service Conflicts: app/assets/javascripts/admin/bulk_product_update.js.coffee --- .../admin/bulk_product_update.js.coffee | 9 +++------ .../admin/directives/toggle_variants.js.coffee | 11 ++++++----- .../admin/services/display_properties.js.coffee | 14 ++++++++++++++ .../bulk_edit/_products_variant.html.haml | 2 +- .../services/display_properties_spec.js.coffee | 17 +++++++++++++++++ 5 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/admin/services/display_properties.js.coffee create mode 100644 spec/javascripts/unit/admin/services/display_properties_spec.js.coffee diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 9694ff761e..fdb1e3be13 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons, SpreeApiKey) -> +angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons, SpreeApiKey) -> $scope.loading = true $scope.updateStatusMessage = @@ -42,7 +42,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.limit = 15 $scope.productsWithUnsavedVariants = [] $scope.query = "" - $scope.displayProperties = {} + $scope.DisplayProperties = DisplayProperties $scope.initialise = -> authorise_api_reponse = "" @@ -70,9 +70,6 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.resetProducts = -> DirtyProducts.clear() $scope.setMessage $scope.updateStatusMessage, "", {}, false - for product in $scope.products - $scope.displayProperties[product.id] ||= showVariants: false - # $scope.matchProducer = (product) -> # for producer in $scope.producers @@ -125,7 +122,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout on_hand: null price: null $scope.productsWithUnsavedVariants.push product - $scope.displayProperties[product.id].showVariants = true + DisplayProperties.setShowVariants product.id, true $scope.nextVariantId = -> diff --git a/app/assets/javascripts/admin/directives/toggle_variants.js.coffee b/app/assets/javascripts/admin/directives/toggle_variants.js.coffee index 879942d698..410df8d7e9 100644 --- a/app/assets/javascripts/admin/directives/toggle_variants.js.coffee +++ b/app/assets/javascripts/admin/directives/toggle_variants.js.coffee @@ -1,18 +1,19 @@ -angular.module("ofn.admin").directive "ofnToggleVariants", -> +angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) -> link: (scope, element, attrs) -> - if scope.displayProperties[scope.product.id].showVariants + if DisplayProperties.showVariants scope.product.id element.removeClass "icon-chevron-right" element.addClass "icon-chevron-down" else element.removeClass "icon-chevron-down" element.addClass "icon-chevron-right" + element.on "click", -> scope.$apply -> - if scope.displayProperties[scope.product.id].showVariants - scope.displayProperties[scope.product.id].showVariants = false + if DisplayProperties.showVariants scope.product.id + DisplayProperties.setShowVariants scope.product.id, false element.removeClass "icon-chevron-down" element.addClass "icon-chevron-right" else - scope.displayProperties[scope.product.id].showVariants = true + DisplayProperties.setShowVariants scope.product.id, true element.removeClass "icon-chevron-right" element.addClass "icon-chevron-down" \ No newline at end of file diff --git a/app/assets/javascripts/admin/services/display_properties.js.coffee b/app/assets/javascripts/admin/services/display_properties.js.coffee new file mode 100644 index 0000000000..7288706032 --- /dev/null +++ b/app/assets/javascripts/admin/services/display_properties.js.coffee @@ -0,0 +1,14 @@ +angular.module("ofn.admin").factory "DisplayProperties", -> + new class DisplayProperties + displayProperties: {} + + showVariants: (product_id) -> + @initProduct product_id + @displayProperties[product_id].showVariants + + setShowVariants: (product_id, showVariants) -> + @initProduct product_id + @displayProperties[product_id].showVariants = showVariants + + initProduct: (product_id) -> + @displayProperties[product_id] ||= {showVariants: false} diff --git a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml b/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml index 6369da0d99..28f6ed6321 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml @@ -1,4 +1,4 @@ -%tr.variant{ :id => "v_{{variant.id}}", 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'displayProperties[product.id].showVariants', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } +%tr.variant{ :id => "v_{{variant.id}}", 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'DisplayProperties.showVariants(product.id)', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } %td.left-actions %a{ :class => "variant-item icon-caret-right", 'ng-hide' => "$last" } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last" } diff --git a/spec/javascripts/unit/admin/services/display_properties_spec.js.coffee b/spec/javascripts/unit/admin/services/display_properties_spec.js.coffee new file mode 100644 index 0000000000..e2a5433903 --- /dev/null +++ b/spec/javascripts/unit/admin/services/display_properties_spec.js.coffee @@ -0,0 +1,17 @@ +describe "DisplayProperties", -> + DisplayProperties = null + + beforeEach -> + module "ofn.admin" + + beforeEach inject (_DisplayProperties_) -> + DisplayProperties = _DisplayProperties_ + + it "defaults showVariants to false", -> + expect(DisplayProperties.showVariants(123)).toEqual false + + it "sets the showVariants value", -> + DisplayProperties.setShowVariants(123, true) + expect(DisplayProperties.showVariants(123)).toEqual true + DisplayProperties.setShowVariants(123, false) + expect(DisplayProperties.showVariants(123)).toEqual false From 7e2c979c9637fd0b891a43f4e7770c4ff8df79fa Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Sep 2014 08:46:50 +1000 Subject: [PATCH 248/681] WIP: Extracting BulkProducts-specific specs to their own file --- .../services/bulk_products_spec.js.coffee | 149 ++++++++++++++ .../unit/bulk_product_update_spec.js.coffee | 188 ++---------------- 2 files changed, 167 insertions(+), 170 deletions(-) create mode 100644 spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee new file mode 100644 index 0000000000..68d9b4c1ba --- /dev/null +++ b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee @@ -0,0 +1,149 @@ + # describe "fetching products", -> + # it "makes a standard call to dataFetcher when no filters exist", -> + # $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products" + # $scope.fetchProducts() + + # it "calls makes more calls to dataFetcher if more pages exist", -> + # $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: [], pages: 2 } + # $httpBackend.expectGET("/api/products/bulk_products?page=2;per_page=20;").respond { products: ["list of products"] } + # $scope.fetchProducts() + # $httpBackend.flush() + + # it "applies filters when they are present", -> + # filter = {property: $scope.filterableColumns[1], predicate:$scope.filterTypes[0], value:"Product1"} + # $scope.currentFilters.push filter # Don't use addFilter as that is not what we are testing + # expect($scope.currentFilters).toEqual [filter] + # $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;q[name_eq]=Product1;").respond "list of products" + # $scope.fetchProducts() + # $httpBackend.flush() + + + # describe "preparing products", -> + # beforeEach -> + # spyOn $scope, "loadVariantUnit" + + # it "initialises display properties for the product", -> + # product = {id: 123} + # $scope.displayProperties = {} + # $scope.unpackProduct product + # expect($scope.displayProperties[123]).toEqual {showVariants: false} + + # it "calls loadVariantUnit for the product", -> + # product = {id: 123} + # $scope.displayProperties = {} + # $scope.unpackProduct product + # expect($scope.loadVariantUnit.calls.length).toEqual 1 + + + # describe "loading variant unit", -> + # describe "setting product variant_unit_with_scale field", -> + # it "sets by combining variant_unit and variant_unit_scale", -> + # product = + # variant_unit: "volume" + # variant_unit_scale: .001 + # $scope.loadVariantUnit product + # expect(product.variant_unit_with_scale).toEqual "volume_0.001" + + # it "sets to null when variant_unit is null", -> + # product = {variant_unit: null, variant_unit_scale: 1000} + # $scope.loadVariantUnit product + # expect(product.variant_unit_with_scale).toBeNull() + + # it "sets to variant_unit when variant_unit_scale is null", -> + # product = {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'} + # $scope.loadVariantUnit product + # expect(product.variant_unit_with_scale).toEqual "items" + + # it "sets to variant_unit when variant_unit is 'items'", -> + # product = {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'} + # $scope.loadVariantUnit product + # expect(product.variant_unit_with_scale).toEqual "items" + + # it "loads data for variants (incl. master)", -> + # spyOn $scope, "loadVariantUnitValues" + # spyOn $scope, "loadVariantUnitValue" + + # product = + # variant_unit_scale: 1.0 + # master: {id: 1, unit_value: 1, unit_description: '(one)'} + # variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] + # $scope.loadVariantUnit product + + # expect($scope.loadVariantUnitValues).toHaveBeenCalledWith product + # expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.master + + # it "loads data for variants (excl. master)", -> + # spyOn $scope, "loadVariantUnitValue" + + # product = + # variant_unit_scale: 1.0 + # master: {id: 1, unit_value: 1, unit_description: '(one)'} + # variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] + # $scope.loadVariantUnitValues product + + # expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0] + # expect($scope.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master + + # describe "setting variant unit_value_with_description", -> + # it "sets by combining unit_value and unit_description", -> + # product = + # variant_unit_scale: 1.0 + # variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}] + # $scope.loadVariantUnitValues product, product.variants[0] + # expect(product.variants[0]).toEqual + # id: 1 + # unit_value: 1 + # unit_description: '(bottle)' + # unit_value_with_description: '1 (bottle)' + + # it "uses unit_value when description is missing", -> + # product = + # variant_unit_scale: 1.0 + # variants: [{id: 1, unit_value: 1}] + # $scope.loadVariantUnitValues product, product.variants[0] + # expect(product.variants[0].unit_value_with_description).toEqual '1' + + # it "uses unit_description when value is missing", -> + # product = + # variant_unit_scale: 1.0 + # variants: [{id: 1, unit_description: 'Small'}] + # $scope.loadVariantUnitValues product, product.variants[0] + # expect(product.variants[0].unit_value_with_description).toEqual 'Small' + + # it "converts values from base value to chosen unit", -> + # product = + # variant_unit_scale: 1000.0 + # variants: [{id: 1, unit_value: 2500}] + # $scope.loadVariantUnitValues product, product.variants[0] + # expect(product.variants[0].unit_value_with_description).toEqual '2.5' + + # it "displays a unit_value of zero", -> + # product = + # variant_unit_scale: 1.0 + # variants: [{id: 1, unit_value: 0}] + # $scope.loadVariantUnitValues product, product.variants[0] + # expect(product.variants[0].unit_value_with_description).toEqual '0' + + + # describe "calculating the scaled unit value for a variant", -> + # it "returns the scaled value when variant has a unit_value", -> + # product = {variant_unit_scale: 0.001} + # variant = {unit_value: 5} + # expect($scope.variantUnitValue(product, variant)).toEqual 5000 + + # it "returns the unscaled value when the product has no scale", -> + # product = {} + # variant = {unit_value: 5} + # expect($scope.variantUnitValue(product, variant)).toEqual 5 + + # it "returns zero when the value is zero", -> + # product = {} + # variant = {unit_value: 0} + # expect($scope.variantUnitValue(product, variant)).toEqual 0 + + # it "returns null when the variant has no unit_value", -> + # product = {} + # variant = {} + # expect($scope.variantUnitValue(product, variant)).toEqual null + + diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index fa7ebdd82d..13cb2951c2 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -231,7 +231,7 @@ describe "filtering products for submission to database", -> ] describe "AdminProductEditCtrl", -> - $ctrl = $scope = $timeout = $httpBackend = DirtyProducts = null + $ctrl = $scope = $timeout = $httpBackend = BulkProducts = DirtyProducts = null beforeEach -> module "ofn.admin" @@ -241,11 +241,12 @@ describe "AdminProductEditCtrl", -> $provide.value 'SpreeApiKey', 'API_KEY' null - beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _DirtyProducts_) -> + beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _BulkProducts_, _DirtyProducts_) -> $scope = $rootScope.$new() $ctrl = _$controller_ $timeout = _$timeout_ $httpBackend = _$httpBackend_ + BulkProducts = _BulkProducts_ DirtyProducts = _DirtyProducts_ $ctrl "AdminProductEditCtrl", {$scope: $scope, $timeout: $timeout} @@ -262,42 +263,33 @@ describe "AdminProductEditCtrl", -> describe "fetching products", -> - it "makes a standard call to dataFetcher when no filters exist", -> - $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products" - $scope.fetchProducts() + $q = null + deferred = null + + beforeEach inject((_$q_) -> + $q = _$q_ + ) + + beforeEach -> + deferred = $q.defer() + deferred.resolve() + spyOn $scope, "resetProducts" + spyOn(BulkProducts, "fetch").andReturn deferred.promise it "calls resetProducts after data has been received", -> - spyOn $scope, "resetProducts" - $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: "list of products" } $scope.fetchProducts() - $httpBackend.flush() - expect($scope.resetProducts).toHaveBeenCalledWith "list of products" - - it "calls makes more calls to dataFetcher if more pages exist", -> - $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: [], pages: 2 } - $httpBackend.expectGET("/api/products/bulk_products?page=2;per_page=20;").respond { products: ["list of products"] } - $scope.fetchProducts() - $httpBackend.flush() - - it "applies filters when they are present", -> - filter = {property: $scope.filterableColumns[1], predicate:$scope.filterTypes[0], value:"Product1"} - $scope.currentFilters.push filter # Don't use addFilter as that is not what we are testing - expect($scope.currentFilters).toEqual [filter] - $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;q[name_eq]=Product1;").respond "list of products" - $scope.fetchProducts() - $httpBackend.flush() + $scope.$digest() + expect($scope.resetProducts).toHaveBeenCalled() it "sets the loading property to true before fetching products and unsets it when loading is complete", -> - $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products" $scope.fetchProducts() expect($scope.loading).toEqual true - $httpBackend.flush() + $scope.$digest() expect($scope.loading).toEqual false describe "resetting products", -> beforeEach -> - spyOn $scope, "unpackProduct" spyOn DirtyProducts, "clear" $scope.products = {} $scope.resetProducts [ @@ -311,153 +303,9 @@ describe "AdminProductEditCtrl", -> } ] - it "sets products to the value of 'data'", -> - expect($scope.products).toEqual [ - { - id: 1 - name: "P1" - } - { - id: 3 - name: "P2" - } - ] - it "resets dirtyProducts", -> expect(DirtyProducts.clear).toHaveBeenCalled() - it "calls unpackProduct once for each product", -> - expect($scope.unpackProduct.calls.length).toEqual 2 - - - describe "preparing products", -> - beforeEach -> - spyOn $scope, "loadVariantUnit" - - it "initialises display properties for the product", -> - product = {id: 123} - $scope.displayProperties = {} - $scope.unpackProduct product - expect($scope.displayProperties[123]).toEqual {showVariants: false} - - it "calls loadVariantUnit for the product", -> - product = {id: 123} - $scope.displayProperties = {} - $scope.unpackProduct product - expect($scope.loadVariantUnit.calls.length).toEqual 1 - - - describe "loading variant unit", -> - describe "setting product variant_unit_with_scale field", -> - it "sets by combining variant_unit and variant_unit_scale", -> - product = - variant_unit: "volume" - variant_unit_scale: .001 - $scope.loadVariantUnit product - expect(product.variant_unit_with_scale).toEqual "volume_0.001" - - it "sets to null when variant_unit is null", -> - product = {variant_unit: null, variant_unit_scale: 1000} - $scope.loadVariantUnit product - expect(product.variant_unit_with_scale).toBeNull() - - it "sets to variant_unit when variant_unit_scale is null", -> - product = {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'} - $scope.loadVariantUnit product - expect(product.variant_unit_with_scale).toEqual "items" - - it "sets to variant_unit when variant_unit is 'items'", -> - product = {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'} - $scope.loadVariantUnit product - expect(product.variant_unit_with_scale).toEqual "items" - - it "loads data for variants (incl. master)", -> - spyOn $scope, "loadVariantUnitValues" - spyOn $scope, "loadVariantUnitValue" - - product = - variant_unit_scale: 1.0 - master: {id: 1, unit_value: 1, unit_description: '(one)'} - variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] - $scope.loadVariantUnit product - - expect($scope.loadVariantUnitValues).toHaveBeenCalledWith product - expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.master - - it "loads data for variants (excl. master)", -> - spyOn $scope, "loadVariantUnitValue" - - product = - variant_unit_scale: 1.0 - master: {id: 1, unit_value: 1, unit_description: '(one)'} - variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] - $scope.loadVariantUnitValues product - - expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0] - expect($scope.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master - - describe "setting variant unit_value_with_description", -> - it "sets by combining unit_value and unit_description", -> - product = - variant_unit_scale: 1.0 - variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}] - $scope.loadVariantUnitValues product, product.variants[0] - expect(product.variants[0]).toEqual - id: 1 - unit_value: 1 - unit_description: '(bottle)' - unit_value_with_description: '1 (bottle)' - - it "uses unit_value when description is missing", -> - product = - variant_unit_scale: 1.0 - variants: [{id: 1, unit_value: 1}] - $scope.loadVariantUnitValues product, product.variants[0] - expect(product.variants[0].unit_value_with_description).toEqual '1' - - it "uses unit_description when value is missing", -> - product = - variant_unit_scale: 1.0 - variants: [{id: 1, unit_description: 'Small'}] - $scope.loadVariantUnitValues product, product.variants[0] - expect(product.variants[0].unit_value_with_description).toEqual 'Small' - - it "converts values from base value to chosen unit", -> - product = - variant_unit_scale: 1000.0 - variants: [{id: 1, unit_value: 2500}] - $scope.loadVariantUnitValues product, product.variants[0] - expect(product.variants[0].unit_value_with_description).toEqual '2.5' - - it "displays a unit_value of zero", -> - product = - variant_unit_scale: 1.0 - variants: [{id: 1, unit_value: 0}] - $scope.loadVariantUnitValues product, product.variants[0] - expect(product.variants[0].unit_value_with_description).toEqual '0' - - - describe "calculating the scaled unit value for a variant", -> - it "returns the scaled value when variant has a unit_value", -> - product = {variant_unit_scale: 0.001} - variant = {unit_value: 5} - expect($scope.variantUnitValue(product, variant)).toEqual 5000 - - it "returns the unscaled value when the product has no scale", -> - product = {} - variant = {unit_value: 5} - expect($scope.variantUnitValue(product, variant)).toEqual 5 - - it "returns zero when the value is zero", -> - product = {} - variant = {unit_value: 0} - expect($scope.variantUnitValue(product, variant)).toEqual 0 - - it "returns null when the variant has no unit_value", -> - product = {} - variant = {} - expect($scope.variantUnitValue(product, variant)).toEqual null - describe "updating the product on hand count", -> it "updates when product is not available on demand", -> From 387d25b8f2d3741f9177919a11109d6008e3c73b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Sep 2014 08:57:48 +1000 Subject: [PATCH 249/681] WIP: More extracting BulkProducts-specific specs to their own file --- .../services/bulk_products_spec.js.coffee | 90 +++++++++++++++ .../unit/bulk_product_update_spec.js.coffee | 108 ++---------------- 2 files changed, 100 insertions(+), 98 deletions(-) diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee index 68d9b4c1ba..13ac6dc2c1 100644 --- a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee @@ -18,6 +18,85 @@ # $httpBackend.flush() + # describe "cloning products", -> + # it "clones products using a http get request to /admin/products/(permalink)/clone.json", -> + # $scope.products = [ + # id: 13 + # permalink_live: "oranges" + # ] + # $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, + # product: + # id: 17 + # name: "new_product" + + # $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, [ + # id: 17 + # name: "new_product" + # ] + # $scope.cloneProduct $scope.products[0] + # $httpBackend.flush() + + # it "adds the newly created product to $scope.products and matches producer", -> + # spyOn($scope, "unpackProduct").andCallThrough() + # $scope.products = [ + # id: 13 + # permalink_live: "oranges" + # ] + # $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, + # product: + # id: 17 + # name: "new_product" + # producer_id: 6 + + # variants: [ + # id: 3 + # name: "V1" + # ] + + # $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, + # id: 17 + # name: "new_product" + # producer_id: 6 + + # variants: [ + # id: 3 + # name: "V1" + # ] + + # $scope.cloneProduct $scope.products[0] + # $httpBackend.flush() + # expect($scope.unpackProduct).toHaveBeenCalledWith + # id: 17 + # name: "new_product" + # variant_unit_with_scale: null + # producer_id: 6 + + # variants: [ + # id: 3 + # name: "V1" + # unit_value_with_description: "" + # ] + + # expect($scope.products).toEqual [ + # { + # id: 13 + # permalink_live: "oranges" + # } + # { + # id: 17 + # name: "new_product" + # variant_unit_with_scale: null + # producer_id: 6 + + # variants: [ + # id: 3 + # name: "V1" + # unit_value_with_description: "" + # ] + # } + # ] + + # describe "preparing products", -> # beforeEach -> # spyOn $scope, "loadVariantUnit" @@ -147,3 +226,14 @@ # expect($scope.variantUnitValue(product, variant)).toEqual null + # describe "fetching a product by id", -> + # it "returns the product when it is present", -> + # product = {id: 123} + # $scope.products = [product] + # expect($scope.findProduct(123, $scope.products)).toEqual product + + # it "returns null when the product is not present", -> + # $scope.products = [] + # expect($scope.findProduct(123, $scope.products)).toBeNull() + + diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 13cb2951c2..a6fb0fa432 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -231,7 +231,7 @@ describe "filtering products for submission to database", -> ] describe "AdminProductEditCtrl", -> - $ctrl = $scope = $timeout = $httpBackend = BulkProducts = DirtyProducts = null + $ctrl = $scope = $timeout = $httpBackend = BulkProducts = DirtyProducts = DisplayProperties = null beforeEach -> module "ofn.admin" @@ -241,13 +241,14 @@ describe "AdminProductEditCtrl", -> $provide.value 'SpreeApiKey', 'API_KEY' null - beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _BulkProducts_, _DirtyProducts_) -> + beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _BulkProducts_, _DirtyProducts_, _DisplayProperties_) -> $scope = $rootScope.$new() $ctrl = _$controller_ $timeout = _$timeout_ $httpBackend = _$httpBackend_ BulkProducts = _BulkProducts_ DirtyProducts = _DirtyProducts_ + DisplayProperties = _DisplayProperties_ $ctrl "AdminProductEditCtrl", {$scope: $scope, $timeout: $timeout} ) @@ -524,7 +525,7 @@ describe "AdminProductEditCtrl", -> testProduct = {id: 123} beforeEach -> - $scope.products = [testProduct] + BulkProducts.products = [testProduct] it "extracts unit_value and unit_description from unit_value_with_description", -> testProduct = {id: 123, variant_unit_scale: 1.0} @@ -586,7 +587,7 @@ describe "AdminProductEditCtrl", -> it "converts value from chosen unit to base unit", -> testProduct = {id: 123, variant_unit_scale: 1000} testVariant = {unit_value_with_description: "250.5"} - $scope.products = [testProduct] + BulkProducts.products = [testProduct] $scope.packVariant(testProduct, testVariant) expect(testVariant).toEqual unit_value: 250500 @@ -596,7 +597,7 @@ describe "AdminProductEditCtrl", -> it "does not convert value when using a non-scaled unit", -> testProduct = {id: 123} testVariant = {unit_value_with_description: "12"} - $scope.products = [testProduct] + BulkProducts.products = [testProduct] $scope.packVariant(testProduct, testVariant) expect(testVariant).toEqual unit_value: 12 @@ -662,7 +663,7 @@ describe "AdminProductEditCtrl", -> it "runs displaySuccess() when post returns success", -> spyOn $scope, "displaySuccess" - spyOn $scope, "updateVariantLists" + spyOn BulkProducts, "updateVariantLists" spyOn DirtyProducts, "clear" $scope.products = [ { @@ -689,7 +690,7 @@ describe "AdminProductEditCtrl", -> $timeout.flush() expect($scope.displaySuccess).toHaveBeenCalled() expect(DirtyProducts.clear).toHaveBeenCalled() - expect($scope.updateVariantLists).toHaveBeenCalled() + expect(BulkProducts.updateVariantLists).toHaveBeenCalled() it "runs displayFailure() when post returns an error", -> spyOn $scope, "displayFailure" @@ -707,20 +708,10 @@ describe "AdminProductEditCtrl", -> $httpBackend.flush() expect(window.alert).toHaveBeenCalledWith("Saving failed with the following error(s):\nan error\n") - describe "fetching a product by id", -> - it "returns the product when it is present", -> - product = {id: 123} - $scope.products = [product] - expect($scope.findProduct(123, $scope.products)).toEqual product - - it "returns null when the product is not present", -> - $scope.products = [] - expect($scope.findProduct(123, $scope.products)).toBeNull() - describe "adding variants", -> beforeEach -> - $scope.displayProperties ||= {123: {}} + spyOn DisplayProperties, 'setShowVariants' it "adds first and subsequent variants", -> product = {id: 123, variants: []} @@ -736,7 +727,7 @@ describe "AdminProductEditCtrl", -> it "shows the variant(s)", -> product = {id: 123, variants: []} $scope.addVariant(product) - expect($scope.displayProperties[123].showVariants).toBe(true) + expect(DisplayProperties.setShowVariants).toHaveBeenCalledWith 123, true describe "deleting products", -> @@ -871,85 +862,6 @@ describe "AdminProductEditCtrl", -> - describe "cloning products", -> - it "clones products using a http get request to /admin/products/(permalink)/clone.json", -> - $scope.products = [ - id: 13 - permalink_live: "oranges" - ] - $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, - product: - id: 17 - name: "new_product" - - $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, [ - id: 17 - name: "new_product" - ] - $scope.cloneProduct $scope.products[0] - $httpBackend.flush() - - it "adds the newly created product to $scope.products and matches producer", -> - spyOn($scope, "unpackProduct").andCallThrough() - $scope.products = [ - id: 13 - permalink_live: "oranges" - ] - $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, - product: - id: 17 - name: "new_product" - producer_id: 6 - - variants: [ - id: 3 - name: "V1" - ] - - $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, - id: 17 - name: "new_product" - producer_id: 6 - - variants: [ - id: 3 - name: "V1" - ] - - $scope.cloneProduct $scope.products[0] - $httpBackend.flush() - expect($scope.unpackProduct).toHaveBeenCalledWith - id: 17 - name: "new_product" - variant_unit_with_scale: null - producer_id: 6 - - variants: [ - id: 3 - name: "V1" - unit_value_with_description: "" - ] - - expect($scope.products).toEqual [ - { - id: 13 - permalink_live: "oranges" - } - { - id: 17 - name: "new_product" - variant_unit_with_scale: null - producer_id: 6 - - variants: [ - id: 3 - name: "V1" - unit_value_with_description: "" - ] - } - ] - - describe "filtering products", -> describe "clearing filters", -> it "resets filter variables", -> From 221e9344fddb891d43a8517f318a6fc4399af45d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Sep 2014 11:37:54 +1000 Subject: [PATCH 250/681] Port fetch products specs to BulkProducts service specs --- .../services/bulk_products_spec.js.coffee | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee index 13ac6dc2c1..bc1911e722 100644 --- a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee @@ -1,21 +1,40 @@ - # describe "fetching products", -> - # it "makes a standard call to dataFetcher when no filters exist", -> - # $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products" - # $scope.fetchProducts() +describe "BulkProducts service", -> + BulkProducts = $httpBackend = null - # it "calls makes more calls to dataFetcher if more pages exist", -> - # $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: [], pages: 2 } - # $httpBackend.expectGET("/api/products/bulk_products?page=2;per_page=20;").respond { products: ["list of products"] } - # $scope.fetchProducts() - # $httpBackend.flush() + beforeEach -> + module "ofn.admin" - # it "applies filters when they are present", -> - # filter = {property: $scope.filterableColumns[1], predicate:$scope.filterTypes[0], value:"Product1"} - # $scope.currentFilters.push filter # Don't use addFilter as that is not what we are testing - # expect($scope.currentFilters).toEqual [filter] - # $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;q[name_eq]=Product1;").respond "list of products" - # $scope.fetchProducts() - # $httpBackend.flush() + beforeEach inject (_BulkProducts_, _$httpBackend_) -> + BulkProducts = _BulkProducts_ + $httpBackend = _$httpBackend_ + + describe "fetching products", -> + beforeEach -> + spyOn BulkProducts, 'addProducts' + + it "makes a standard call to dataFetcher when no filters exist", -> + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products" + BulkProducts.fetch [], -> + $httpBackend.flush() + + it "makes more calls to dataFetcher if more pages exist", -> + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: [], pages: 2 } + $httpBackend.expectGET("/api/products/bulk_products?page=2;per_page=20;").respond { products: ["list of products"] } + BulkProducts.fetch [], -> + $httpBackend.flush() + + it "applies filters when they are supplied", -> + filter = + property: + name: "Name" + db_column: "name" + predicate: + name: "Equals" + predicate: "eq" + value: "Product1" + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;q[name_eq]=Product1;").respond "list of products" + BulkProducts.fetch [filter], -> + $httpBackend.flush() # describe "cloning products", -> From 5eb40261a86d067324ee493620a5fd8b5840c82a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Sep 2014 11:57:00 +1000 Subject: [PATCH 251/681] Port clone products specs to BulkProducts service specs --- .../admin/services/bulk_products.js.coffee | 6 +- .../services/bulk_products_spec.js.coffee | 105 +++++------------- 2 files changed, 32 insertions(+), 79 deletions(-) diff --git a/app/assets/javascripts/admin/services/bulk_products.js.coffee b/app/assets/javascripts/admin/services/bulk_products.js.coffee index dd2e6536af..ae845c23b9 100644 --- a/app/assets/javascripts/admin/services/bulk_products.js.coffee +++ b/app/assets/javascripts/admin/services/bulk_products.js.coffee @@ -22,10 +22,8 @@ angular.module("ofn.admin").factory "BulkProducts", (dataFetcher) -> # respond_with block in the destroy action of Spree::Admin::Product to break # when a respond_overrride for the clone action is used. id = data.product.id - dataFetcher("/api/products/" + id + "?template=bulk_show").then (data) => - newProduct = data - @unpackProduct newProduct - @products.push newProduct + dataFetcher("/api/products/" + id + "?template=bulk_show").then (newProduct) => + @addProducts [newProduct] updateVariantLists: (serverProducts, productsWithUnsavedVariants) -> for product in productsWithUnsavedVariants diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee index bc1911e722..1602b755ac 100644 --- a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee @@ -37,83 +37,38 @@ describe "BulkProducts service", -> $httpBackend.flush() - # describe "cloning products", -> - # it "clones products using a http get request to /admin/products/(permalink)/clone.json", -> - # $scope.products = [ - # id: 13 - # permalink_live: "oranges" - # ] - # $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, - # product: - # id: 17 - # name: "new_product" + describe "cloning products", -> + it "clones products using a http get request to /admin/products/(permalink)/clone.json", -> + BulkProducts.products = [ + id: 13 + permalink_live: "oranges" + ] + $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, + product: + id: 17 + name: "new_product" + $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, [ + id: 17 + name: "new_product" + ] + BulkProducts.cloneProduct BulkProducts.products[0] + $httpBackend.flush() - # $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, [ - # id: 17 - # name: "new_product" - # ] - # $scope.cloneProduct $scope.products[0] - # $httpBackend.flush() + it "adds the product", -> + originalProduct = + id: 16 + permalink_live: "oranges" + clonedProduct = + id: 17 - # it "adds the newly created product to $scope.products and matches producer", -> - # spyOn($scope, "unpackProduct").andCallThrough() - # $scope.products = [ - # id: 13 - # permalink_live: "oranges" - # ] - # $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, - # product: - # id: 17 - # name: "new_product" - # producer_id: 6 - - # variants: [ - # id: 3 - # name: "V1" - # ] - - # $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, - # id: 17 - # name: "new_product" - # producer_id: 6 - - # variants: [ - # id: 3 - # name: "V1" - # ] - - # $scope.cloneProduct $scope.products[0] - # $httpBackend.flush() - # expect($scope.unpackProduct).toHaveBeenCalledWith - # id: 17 - # name: "new_product" - # variant_unit_with_scale: null - # producer_id: 6 - - # variants: [ - # id: 3 - # name: "V1" - # unit_value_with_description: "" - # ] - - # expect($scope.products).toEqual [ - # { - # id: 13 - # permalink_live: "oranges" - # } - # { - # id: 17 - # name: "new_product" - # variant_unit_with_scale: null - # producer_id: 6 - - # variants: [ - # id: 3 - # name: "V1" - # unit_value_with_description: "" - # ] - # } - # ] + spyOn(BulkProducts, "addProducts") + BulkProducts.products = [originalProduct] + $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, + product: clonedProduct + $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, clonedProduct + BulkProducts.cloneProduct BulkProducts.products[0] + $httpBackend.flush() + expect(BulkProducts.addProducts).toHaveBeenCalledWith [clonedProduct] # describe "preparing products", -> From bf6d0a2beb09792b1650953bbf0c56714eb79c70 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Sep 2014 12:43:31 +1000 Subject: [PATCH 252/681] Port remaining specs to BulkProducts service specs --- .../services/bulk_products_spec.js.coffee | 221 +++++++++--------- 1 file changed, 106 insertions(+), 115 deletions(-) diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee index 1602b755ac..70c22c80e0 100644 --- a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee @@ -71,143 +71,134 @@ describe "BulkProducts service", -> expect(BulkProducts.addProducts).toHaveBeenCalledWith [clonedProduct] - # describe "preparing products", -> - # beforeEach -> - # spyOn $scope, "loadVariantUnit" + describe "preparing products", -> + beforeEach -> + spyOn BulkProducts, "loadVariantUnit" - # it "initialises display properties for the product", -> - # product = {id: 123} - # $scope.displayProperties = {} - # $scope.unpackProduct product - # expect($scope.displayProperties[123]).toEqual {showVariants: false} - - # it "calls loadVariantUnit for the product", -> - # product = {id: 123} - # $scope.displayProperties = {} - # $scope.unpackProduct product - # expect($scope.loadVariantUnit.calls.length).toEqual 1 + it "calls loadVariantUnit for the product", -> + product = {id: 123} + BulkProducts.unpackProduct product + expect(BulkProducts.loadVariantUnit).toHaveBeenCalled() - # describe "loading variant unit", -> - # describe "setting product variant_unit_with_scale field", -> - # it "sets by combining variant_unit and variant_unit_scale", -> - # product = - # variant_unit: "volume" - # variant_unit_scale: .001 - # $scope.loadVariantUnit product - # expect(product.variant_unit_with_scale).toEqual "volume_0.001" + describe "loading variant unit", -> + describe "setting product variant_unit_with_scale field", -> + it "sets by combining variant_unit and variant_unit_scale", -> + product = + variant_unit: "volume" + variant_unit_scale: .001 + BulkProducts.loadVariantUnit product + expect(product.variant_unit_with_scale).toEqual "volume_0.001" - # it "sets to null when variant_unit is null", -> - # product = {variant_unit: null, variant_unit_scale: 1000} - # $scope.loadVariantUnit product - # expect(product.variant_unit_with_scale).toBeNull() + it "sets to null when variant_unit is null", -> + product = {variant_unit: null, variant_unit_scale: 1000} + BulkProducts.loadVariantUnit product + expect(product.variant_unit_with_scale).toBeNull() - # it "sets to variant_unit when variant_unit_scale is null", -> - # product = {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'} - # $scope.loadVariantUnit product - # expect(product.variant_unit_with_scale).toEqual "items" + it "sets to variant_unit when variant_unit_scale is null", -> + product = {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'} + BulkProducts.loadVariantUnit product + expect(product.variant_unit_with_scale).toEqual "items" - # it "sets to variant_unit when variant_unit is 'items'", -> - # product = {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'} - # $scope.loadVariantUnit product - # expect(product.variant_unit_with_scale).toEqual "items" + it "sets to variant_unit when variant_unit is 'items'", -> + product = {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'} + BulkProducts.loadVariantUnit product + expect(product.variant_unit_with_scale).toEqual "items" - # it "loads data for variants (incl. master)", -> - # spyOn $scope, "loadVariantUnitValues" - # spyOn $scope, "loadVariantUnitValue" + it "loads data for variants (incl. master)", -> + spyOn BulkProducts, "loadVariantUnitValues" + spyOn BulkProducts, "loadVariantUnitValue" - # product = - # variant_unit_scale: 1.0 - # master: {id: 1, unit_value: 1, unit_description: '(one)'} - # variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] - # $scope.loadVariantUnit product + product = + variant_unit_scale: 1.0 + master: {id: 1, unit_value: 1, unit_description: '(one)'} + variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] + BulkProducts.loadVariantUnit product - # expect($scope.loadVariantUnitValues).toHaveBeenCalledWith product - # expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.master + expect(BulkProducts.loadVariantUnitValues).toHaveBeenCalledWith product + expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.master - # it "loads data for variants (excl. master)", -> - # spyOn $scope, "loadVariantUnitValue" + it "loads data for variants (excl. master)", -> + spyOn BulkProducts, "loadVariantUnitValue" - # product = - # variant_unit_scale: 1.0 - # master: {id: 1, unit_value: 1, unit_description: '(one)'} - # variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] - # $scope.loadVariantUnitValues product + product = + variant_unit_scale: 1.0 + master: {id: 1, unit_value: 1, unit_description: '(one)'} + variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] + BulkProducts.loadVariantUnitValues product - # expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0] - # expect($scope.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master + expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0] + expect(BulkProducts.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master - # describe "setting variant unit_value_with_description", -> - # it "sets by combining unit_value and unit_description", -> - # product = - # variant_unit_scale: 1.0 - # variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}] - # $scope.loadVariantUnitValues product, product.variants[0] - # expect(product.variants[0]).toEqual - # id: 1 - # unit_value: 1 - # unit_description: '(bottle)' - # unit_value_with_description: '1 (bottle)' + describe "setting variant unit_value_with_description", -> + it "sets by combining unit_value and unit_description", -> + product = + variant_unit_scale: 1.0 + variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}] + BulkProducts.loadVariantUnitValues product, product.variants[0] + expect(product.variants[0]).toEqual + id: 1 + unit_value: 1 + unit_description: '(bottle)' + unit_value_with_description: '1 (bottle)' - # it "uses unit_value when description is missing", -> - # product = - # variant_unit_scale: 1.0 - # variants: [{id: 1, unit_value: 1}] - # $scope.loadVariantUnitValues product, product.variants[0] - # expect(product.variants[0].unit_value_with_description).toEqual '1' + it "uses unit_value when description is missing", -> + product = + variant_unit_scale: 1.0 + variants: [{id: 1, unit_value: 1}] + BulkProducts.loadVariantUnitValues product, product.variants[0] + expect(product.variants[0].unit_value_with_description).toEqual '1' - # it "uses unit_description when value is missing", -> - # product = - # variant_unit_scale: 1.0 - # variants: [{id: 1, unit_description: 'Small'}] - # $scope.loadVariantUnitValues product, product.variants[0] - # expect(product.variants[0].unit_value_with_description).toEqual 'Small' + it "uses unit_description when value is missing", -> + product = + variant_unit_scale: 1.0 + variants: [{id: 1, unit_description: 'Small'}] + BulkProducts.loadVariantUnitValues product, product.variants[0] + expect(product.variants[0].unit_value_with_description).toEqual 'Small' - # it "converts values from base value to chosen unit", -> - # product = - # variant_unit_scale: 1000.0 - # variants: [{id: 1, unit_value: 2500}] - # $scope.loadVariantUnitValues product, product.variants[0] - # expect(product.variants[0].unit_value_with_description).toEqual '2.5' + it "converts values from base value to chosen unit", -> + product = + variant_unit_scale: 1000.0 + variants: [{id: 1, unit_value: 2500}] + BulkProducts.loadVariantUnitValues product, product.variants[0] + expect(product.variants[0].unit_value_with_description).toEqual '2.5' - # it "displays a unit_value of zero", -> - # product = - # variant_unit_scale: 1.0 - # variants: [{id: 1, unit_value: 0}] - # $scope.loadVariantUnitValues product, product.variants[0] - # expect(product.variants[0].unit_value_with_description).toEqual '0' + it "displays a unit_value of zero", -> + product = + variant_unit_scale: 1.0 + variants: [{id: 1, unit_value: 0}] + BulkProducts.loadVariantUnitValues product, product.variants[0] + expect(product.variants[0].unit_value_with_description).toEqual '0' - # describe "calculating the scaled unit value for a variant", -> - # it "returns the scaled value when variant has a unit_value", -> - # product = {variant_unit_scale: 0.001} - # variant = {unit_value: 5} - # expect($scope.variantUnitValue(product, variant)).toEqual 5000 + describe "calculating the scaled unit value for a variant", -> + it "returns the scaled value when variant has a unit_value", -> + product = {variant_unit_scale: 0.001} + variant = {unit_value: 5} + expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5000 - # it "returns the unscaled value when the product has no scale", -> - # product = {} - # variant = {unit_value: 5} - # expect($scope.variantUnitValue(product, variant)).toEqual 5 + it "returns the unscaled value when the product has no scale", -> + product = {} + variant = {unit_value: 5} + expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5 - # it "returns zero when the value is zero", -> - # product = {} - # variant = {unit_value: 0} - # expect($scope.variantUnitValue(product, variant)).toEqual 0 + it "returns zero when the value is zero", -> + product = {} + variant = {unit_value: 0} + expect(BulkProducts.variantUnitValue(product, variant)).toEqual 0 - # it "returns null when the variant has no unit_value", -> - # product = {} - # variant = {} - # expect($scope.variantUnitValue(product, variant)).toEqual null + it "returns null when the variant has no unit_value", -> + product = {} + variant = {} + expect(BulkProducts.variantUnitValue(product, variant)).toEqual null - # describe "fetching a product by id", -> - # it "returns the product when it is present", -> - # product = {id: 123} - # $scope.products = [product] - # expect($scope.findProduct(123, $scope.products)).toEqual product - - # it "returns null when the product is not present", -> - # $scope.products = [] - # expect($scope.findProduct(123, $scope.products)).toBeNull() - + describe "fetching a product by id", -> + it "returns the product when it is present", -> + product = {id: 123} + BulkProducts.products = [product] + expect(BulkProducts.find(123)).toEqual product + it "returns null when the product is not present", -> + BulkProducts.products = [] + expect(BulkProducts.find(123)).toBeNull() From 04b065e28a8702b22357c38185a47c4fe330e6b3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 14 Nov 2014 16:48:33 +1100 Subject: [PATCH 253/681] Orders page doesn't crash when orders have no distributor --- .../admin/orders/index/add_distributor_td.html.haml.deface | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface b/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface index af19c08ddd..5d9b7ab083 100644 --- a/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface +++ b/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface @@ -1,4 +1,4 @@ / insert_top "[data-hook='admin_orders_index_rows']" %td.align-center - = order.distributor.name + = order.distributor.andand.name From 2c4b8d779c149dcaa84dc6b4efd4015b4ee25cad Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 21 Nov 2014 10:17:43 +1100 Subject: [PATCH 254/681] Make application.yml available to travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 90328e3a8a..0a0a55517f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ before_install: before_script: - cp config/database.travis.yml config/database.yml - psql -c 'create database open_food_network_test;' -U postgres + - cp config/application.yml.example config/application.yml script: - RAILS_ENV=test bundle exec rake db:migrate --trace - bundle exec rake spec From 5e3f1e4a3b73e493c7ae0ceafadb29f979ce2687 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 21 Nov 2014 12:37:56 +1100 Subject: [PATCH 255/681] Adding bugsnag notifier to work out what is happening with bug when adding variants to order --- app/models/spree/order_decorator.rb | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index fcc6054f3b..6b1d8fbfda 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -21,7 +21,7 @@ Spree::Order.class_eval do go_to_state :delivery go_to_state :payment, :if => lambda { |order| # Fix for #2191 - if order.shipping_method.andand.require_ship_address and + if order.shipping_method.andand.require_ship_address and if order.ship_address.andand.valid? order.create_shipment! order.update_totals @@ -76,10 +76,10 @@ Spree::Order.class_eval do errors.add(:distributor_id, "cannot supply the products in your cart") unless DistributionChangeValidator.new(self).can_change_to_distributor?(distributor) end end - + def empty_with_clear_shipping_and_payments! empty_without_clear_shipping_and_payments! - payments.clear + payments.clear update_attributes(shipping_method_id: nil) end alias_method_chain :empty!, :clear_shipping_and_payments @@ -97,8 +97,20 @@ Spree::Order.class_eval do def add_variant(variant, quantity = 1, max_quantity = nil, currency = nil) current_item = find_line_item_by_variant(variant) if current_item - current_item.quantity += quantity - current_item.max_quantity += max_quantity.to_i + Bugsnag.notify(RuntimeError.new("Order populator weirdness"), { + current_item: current_item.as_json.pretty_inspect, + line_items: line_items.map(&:id), + reloaded: line_items(:reload).map(&:id), + variant: variant.as_json.pretty_inspect + }) + current_item.quantity = quantity + current_item.max_quantity = max_quantity + + # This is the original behaviour, behaviour above is so that we can resolve the order populator bug + # current_item.quantity ||= 0 + # current_item.max_quantity ||= 0 + # current_item.quantity += quantity.to_i + # current_item.max_quantity += max_quantity.to_i current_item.currency = currency unless currency.nil? current_item.save else @@ -170,7 +182,7 @@ Spree::Order.class_eval do # Show payment methods for this distributor def available_payment_methods - @available_payment_methods ||= Spree::PaymentMethod.available(:front_end).select do |pm| + @available_payment_methods ||= Spree::PaymentMethod.available(:front_end).select do |pm| (self.distributor && (pm.distributors.include? self.distributor)) end end From f9b4c07219f8395b6ffb84549544f8015ee1c2d8 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 13 Nov 2014 16:40:04 +1100 Subject: [PATCH 256/681] Add initial VariantOverride model with price lookup --- app/models/variant_override.rb | 8 ++++++++ .../20141113053004_create_variant_overrides.rb | 15 +++++++++++++++ db/schema.rb | 14 +++++++++++++- spec/models/variant_override_spec.rb | 17 +++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 app/models/variant_override.rb create mode 100644 db/migrate/20141113053004_create_variant_overrides.rb create mode 100644 spec/models/variant_override_spec.rb diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb new file mode 100644 index 0000000000..6d20a95571 --- /dev/null +++ b/app/models/variant_override.rb @@ -0,0 +1,8 @@ +class VariantOverride < ActiveRecord::Base + belongs_to :variant, class_name: 'Spree::Variant' + belongs_to :hub, class_name: 'Enterprise' + + def self.price_for(variant, hub) + VariantOverride.where(variant_id: variant, hub_id: hub).first.andand.price + end +end diff --git a/db/migrate/20141113053004_create_variant_overrides.rb b/db/migrate/20141113053004_create_variant_overrides.rb new file mode 100644 index 0000000000..ae3746f828 --- /dev/null +++ b/db/migrate/20141113053004_create_variant_overrides.rb @@ -0,0 +1,15 @@ +class CreateVariantOverrides < ActiveRecord::Migration + def change + create_table :variant_overrides do |t| + t.references :variant + t.references :hub + t.decimal :price, precision: 8, scale: 2 + t.integer :count_on_hand + end + + add_foreign_key :variant_overrides, :spree_variants, column: :variant_id + add_foreign_key :variant_overrides, :enterprises, column: :hub_id + + add_index :variant_overrides, [:variant_id, :hub_id] + end +end diff --git a/db/schema.rb b/db/schema.rb index a3245f549e..901705bbbc 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 => 20141023050324) do +ActiveRecord::Schema.define(:version => 20141113053004) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -1033,6 +1033,15 @@ ActiveRecord::Schema.define(:version => 20141023050324) do t.integer "state_id" end + create_table "variant_overrides", :force => true do |t| + t.integer "variant_id" + t.integer "hub_id" + t.decimal "price", :precision => 8, :scale => 2 + t.integer "count_on_hand" + end + + add_index "variant_overrides", ["variant_id", "hub_id"], :name => "index_variant_overrides_on_variant_id_and_hub_id" + add_foreign_key "adjustment_metadata", "enterprises", name: "adjustment_metadata_enterprise_id_fk" add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id" @@ -1190,4 +1199,7 @@ ActiveRecord::Schema.define(:version => 20141023050324) do add_foreign_key "suburbs", "spree_states", name: "suburbs_state_id_fk", column: "state_id" + add_foreign_key "variant_overrides", "enterprises", name: "variant_overrides_hub_id_fk", column: "hub_id" + add_foreign_key "variant_overrides", "spree_variants", name: "variant_overrides_variant_id_fk", column: "variant_id" + end diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb new file mode 100644 index 0000000000..093ed6007e --- /dev/null +++ b/spec/models/variant_override_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe VariantOverride do + describe "looking up prices" do + let(:variant) { create(:variant) } + let(:hub) { create(:distributor_enterprise) } + + it "returns the numeric price when present" do + VariantOverride.create!(variant: variant, hub: hub, price: 12.34) + VariantOverride.price_for(variant, hub).should == 12.34 + end + + it "returns nil otherwise" do + VariantOverride.price_for(variant, hub).should be_nil + end + end +end From f3fa5edb9da4939fd80cfa153c8a43c4ee30980f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 13 Nov 2014 17:14:29 +1100 Subject: [PATCH 257/681] Add simple variant proxy --- lib/open_food_network/variant_proxy.rb | 19 +++++++++++++ .../open_food_network/variant_proxy_spec.rb | 27 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 lib/open_food_network/variant_proxy.rb create mode 100644 spec/lib/open_food_network/variant_proxy_spec.rb diff --git a/lib/open_food_network/variant_proxy.rb b/lib/open_food_network/variant_proxy.rb new file mode 100644 index 0000000000..9764cfde06 --- /dev/null +++ b/lib/open_food_network/variant_proxy.rb @@ -0,0 +1,19 @@ +module OpenFoodNetwork + class VariantProxy + instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ } + + def initialize(variant, hub) + @variant = variant + @hub = hub + end + + def price + VariantOverride.price_for(@variant, @hub) || @variant.price + end + + + def method_missing(name, *args, &block) + @variant.send(name, *args, &block) + end + end +end diff --git a/spec/lib/open_food_network/variant_proxy_spec.rb b/spec/lib/open_food_network/variant_proxy_spec.rb new file mode 100644 index 0000000000..6439c502a5 --- /dev/null +++ b/spec/lib/open_food_network/variant_proxy_spec.rb @@ -0,0 +1,27 @@ +require 'open_food_network/variant_proxy' + +module OpenFoodNetwork + describe VariantProxy do + let(:hub) { double(:hub) } + let(:v) { double(:variant, sku: 'sku123', price: 'global price') } + let(:vp) { VariantProxy.new(v, hub) } + + describe "delegating calls to proxied variant" do + it "delegates sku" do + vp.sku.should == 'sku123' + end + end + + describe "looking up the price" do + it "returns the override price when there is one" do + VariantOverride.stub(:price_for) { 'override price' } + vp.price.should == 'override price' + end + + it "returns the global price otherwise" do + VariantOverride.stub(:price_for) { nil } + vp.price.should == 'global price' + end + end + end +end From 2b0f6b786584199c6831de240c4cfbbb6f87bd3c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 13 Nov 2014 17:34:31 +1100 Subject: [PATCH 258/681] Add ProductProxy which wraps the product's variants in VariantProxys --- lib/open_food_network/product_proxy.rb | 18 +++++++++++++ .../open_food_network/product_proxy_spec.rb | 27 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 lib/open_food_network/product_proxy.rb create mode 100644 spec/lib/open_food_network/product_proxy_spec.rb diff --git a/lib/open_food_network/product_proxy.rb b/lib/open_food_network/product_proxy.rb new file mode 100644 index 0000000000..e7f06f0019 --- /dev/null +++ b/lib/open_food_network/product_proxy.rb @@ -0,0 +1,18 @@ +module OpenFoodNetwork + class ProductProxy + instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ } + + def initialize(product, hub) + @product = product + @hub = hub + end + + def variants + @product.variants.map { |v| VariantProxy.new(v, @hub) } + end + + def method_missing(name, *args, &block) + @product.send(name, *args, &block) + end + end +end diff --git a/spec/lib/open_food_network/product_proxy_spec.rb b/spec/lib/open_food_network/product_proxy_spec.rb new file mode 100644 index 0000000000..e8a8565949 --- /dev/null +++ b/spec/lib/open_food_network/product_proxy_spec.rb @@ -0,0 +1,27 @@ +require 'open_food_network/product_proxy' +require 'open_food_network/variant_proxy' + +module OpenFoodNetwork + describe ProductProxy do + let(:hub) { double(:hub) } + let(:p) { double(:product, name: 'name') } + let(:pp) { ProductProxy.new(p, hub) } + + describe "delegating calls to proxied product" do + it "delegates name" do + pp.name.should == 'name' + end + end + + describe "fetching the variants" do + let(:v1) { double(:variant) } + let(:v2) { double(:variant) } + let(:p) { double(:product, variants: [v1, v2]) } + + it "returns variants wrapped in VariantProxy" do + # #class is proxied too, so we test that it worked by #object_id + pp.variants.map(&:object_id).sort.should_not == [v1.object_id, v2.object_id].sort + end + end + end +end From bad5d798bfff480ed302a6285bb74a36ade6ccdb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 13 Nov 2014 17:34:41 +1100 Subject: [PATCH 259/681] Comment proxy classes --- lib/open_food_network/product_proxy.rb | 3 +++ lib/open_food_network/variant_proxy.rb | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/lib/open_food_network/product_proxy.rb b/lib/open_food_network/product_proxy.rb index e7f06f0019..ecbce189a4 100644 --- a/lib/open_food_network/product_proxy.rb +++ b/lib/open_food_network/product_proxy.rb @@ -1,4 +1,7 @@ module OpenFoodNetwork + # Variants can have several fields overridden on a per-enterprise basis by the + # VariantOverride model. These overrides can be applied to variants by wrapping their + # products in this proxy, which wraps the product's variants in VariantProxy. class ProductProxy instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ } diff --git a/lib/open_food_network/variant_proxy.rb b/lib/open_food_network/variant_proxy.rb index 9764cfde06..26e6796e9a 100644 --- a/lib/open_food_network/variant_proxy.rb +++ b/lib/open_food_network/variant_proxy.rb @@ -1,4 +1,8 @@ module OpenFoodNetwork + # Variants can have several fields overridden on a per-enterprise basis by the + # VariantOverride model. These overrides can be applied to variants by wrapping in an + # instance of the VariantProxy class. This class proxies most methods back to the wrapped + # variant, but checks for overrides when fetching some properties. class VariantProxy instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ } From d81038824839068e5cf2faecaa436bef74876032 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 20 Nov 2014 09:28:25 +1100 Subject: [PATCH 260/681] Add have_select2 capybara matcher --- spec/support/matchers/select2_matchers.rb | 42 +++++++++++++++++++++++ spec/support/request/web_helper.rb | 1 + 2 files changed, 43 insertions(+) create mode 100644 spec/support/matchers/select2_matchers.rb diff --git a/spec/support/matchers/select2_matchers.rb b/spec/support/matchers/select2_matchers.rb new file mode 100644 index 0000000000..af7a5b4aff --- /dev/null +++ b/spec/support/matchers/select2_matchers.rb @@ -0,0 +1,42 @@ +RSpec::Matchers.define :have_select2 do |id, options={}| + + # TODO: Implement other have_select options + # http://www.rubydoc.info/github/jnicklas/capybara/Capybara/Node/Matchers#has_select%3F-instance_method + # TODO: Instead of passing in id, use a more general locator + + match_for_should do |node| + @id, @options = id, options + + #id = find_label_by_text(locator) + from = "#s2id_#{id}" + + found_element = node.has_selector? from + + if !found_element + false + + else + if options.key? :with_options + find(from).click + + all_options_present = options[:with_options].all? do |option| + node.has_selector? "div.select2-drop-active ul.select2-results li", text: option + end + + find(from).click + + all_options_present + end + end + end + + failure_message_for_should do |actual| + message = "expected to find select2 ##{@id}" + message += " with #{@options.inspect}" if @options.any? + message + end + + match_for_should_not do |node| + raise "Not yet implemented" + end +end diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index 870cf248c4..a4f3b5fd00 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -129,6 +129,7 @@ module WebHelper targetted_select2(value, options) end + # Deprecated: Use have_select2 instead (spec/support/matchers/select2_matchers.rb) def have_select2_option(value, options) container = options[:dropdown_css] || ".select2-with-searchbox" page.execute_script %Q{$('#{options[:from]}').select2('open')} From 2c74a94e313e3aadde19f41dce8ca6daef428054 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 20 Nov 2014 09:33:26 +1100 Subject: [PATCH 261/681] Show list of hubs to select for managing variant overrides --- .../override_variants_controller.js.coffee | 2 ++ .../admin/products_controller_decorator.rb | 7 +++++ ...add_override_variants_tab.html.haml.deface | 3 +++ .../products/override_variants.html.haml | 5 ++++ .../override_variants/_data.html.haml | 1 + .../override_variants/_header.html.haml | 4 +++ .../override_variants/_hub_choice.html.haml | 7 +++++ config/routes.rb | 1 + spec/features/admin/override_variants_spec.rb | 26 +++++++++++++++++++ 9 files changed, 56 insertions(+) create mode 100644 app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee create mode 100644 app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface create mode 100644 app/views/spree/admin/products/override_variants.html.haml create mode 100644 app/views/spree/admin/products/override_variants/_data.html.haml create mode 100644 app/views/spree/admin/products/override_variants/_header.html.haml create mode 100644 app/views/spree/admin/products/override_variants/_hub_choice.html.haml create mode 100644 spec/features/admin/override_variants_spec.rb diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee new file mode 100644 index 0000000000..a2de809d8c --- /dev/null +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -0,0 +1,2 @@ +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Enterprises) -> + $scope.Enterprises = Enterprises diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 8561704686..25b25c6b8f 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -2,6 +2,7 @@ require 'open_food_network/spree_api_key_loader' Spree::Admin::ProductsController.class_eval do include OpenFoodNetwork::SpreeApiKeyLoader + include OrderCyclesHelper before_filter :load_form_data, :only => [:bulk_edit, :new, :create, :edit, :update] before_filter :load_spree_api_key, :only => :bulk_edit @@ -48,6 +49,12 @@ Spree::Admin::ProductsController.class_eval do end end + def override_variants + @all_enterprises = [] + @my_enterprises = order_cycle_permitted_enterprises.by_name + end + + protected def location_after_save if URI(request.referer).path == '/admin/products/bulk_edit' diff --git a/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface b/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface new file mode 100644 index 0000000000..5b4bb2983e --- /dev/null +++ b/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_bottom "[data-hook='admin_product_sub_tabs']" + += tab :override_details, url: override_variants_admin_products_path, match_path: '/products/override_variants' diff --git a/app/views/spree/admin/products/override_variants.html.haml b/app/views/spree/admin/products/override_variants.html.haml new file mode 100644 index 0000000000..6f9ffafb31 --- /dev/null +++ b/app/views/spree/admin/products/override_variants.html.haml @@ -0,0 +1,5 @@ += render 'spree/admin/products/override_variants/header' += render 'spree/admin/products/override_variants/data' + +%div{ ng: { app: 'ofn.admin', controller: 'AdminOverrideVariantsCtrl', init: 'initialise()' } } + = render 'spree/admin/products/override_variants/hub_choice' diff --git a/app/views/spree/admin/products/override_variants/_data.html.haml b/app/views/spree/admin/products/override_variants/_data.html.haml new file mode 100644 index 0000000000..6ff3621ab4 --- /dev/null +++ b/app/views/spree/admin/products/override_variants/_data.html.haml @@ -0,0 +1 @@ += admin_inject_enterprises diff --git a/app/views/spree/admin/products/override_variants/_header.html.haml b/app/views/spree/admin/products/override_variants/_header.html.haml new file mode 100644 index 0000000000..7b4a38db47 --- /dev/null +++ b/app/views/spree/admin/products/override_variants/_header.html.haml @@ -0,0 +1,4 @@ +- content_for :page_title do + Override Product Details + += render :partial => 'spree/admin/shared/product_sub_menu' diff --git a/app/views/spree/admin/products/override_variants/_hub_choice.html.haml b/app/views/spree/admin/products/override_variants/_hub_choice.html.haml new file mode 100644 index 0000000000..a2519b7312 --- /dev/null +++ b/app/views/spree/admin/products/override_variants/_hub_choice.html.haml @@ -0,0 +1,7 @@ +.row + .two.columns.alpha + Hub + .four.columns + %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', 'ng-options' => 'hub.id as hub.name for hub in Enterprises.my_enterprises' } + .ten.columns.omega + %input{ type: 'button', value: 'Go', 'ng-click' => 'selectHub()' } diff --git a/config/routes.rb b/config/routes.rb index a860ec8acf..75380a55f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -118,6 +118,7 @@ Spree::Core::Engine.routes.prepend do match '/admin/reports/orders_and_fulfillment' => 'admin/reports#orders_and_fulfillment', :as => "orders_and_fulfillment_admin_reports", :via => [:get, :post] match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post] match '/admin/products/bulk_edit' => 'admin/products#bulk_edit', :as => "bulk_edit_admin_products" + match '/admin/products/override_variants' => 'admin/products#override_variants', :as => "override_variants_admin_products" match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management" match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post] match '/admin/reports/customers' => 'admin/reports#customers', :as => "customers_admin_reports", :via => [:get, :post] diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb new file mode 100644 index 0000000000..5df70b200b --- /dev/null +++ b/spec/features/admin/override_variants_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +feature %q{ + As an Administrator + With products I can add to order cycles + I want to override the stock level and price of those products + Without affecting other hubs that share the same products +}, js: true do + include AuthenticationWorkflow + include WebHelper + + before do + login_to_admin_section + end + + use_short_wait + + describe "selecting a hub" do + let!(:hub) { create(:distributor_enterprise) } + + it "displays a list of hub choices" do + visit '/admin/products/override_variants' + page.should have_select2 'hub_id', with_options: [hub.name] + end + end +end From 4c9aa96b17cff8dd3599e50c3fc13526ad4b595f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 20 Nov 2014 09:52:19 +1100 Subject: [PATCH 262/681] have_select2 can test for an exact set of options --- spec/support/matchers/select2_matchers.rb | 46 +++++++++++++++-------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/spec/support/matchers/select2_matchers.rb b/spec/support/matchers/select2_matchers.rb index af7a5b4aff..8b7bc2f834 100644 --- a/spec/support/matchers/select2_matchers.rb +++ b/spec/support/matchers/select2_matchers.rb @@ -5,29 +5,21 @@ RSpec::Matchers.define :have_select2 do |id, options={}| # TODO: Instead of passing in id, use a more general locator match_for_should do |node| - @id, @options = id, options + @id, @options, @node = id, options, node #id = find_label_by_text(locator) from = "#s2id_#{id}" - found_element = node.has_selector? from + results = [] - if !found_element - false + results << node.has_selector?(from) - else - if options.key? :with_options - find(from).click - - all_options_present = options[:with_options].all? do |option| - node.has_selector? "div.select2-drop-active ul.select2-results li", text: option - end - - find(from).click - - all_options_present - end + if results.all? + results << all_options_present(from, options[:with_options]) if options.key? :with_options + results << exact_options_present(from, options[:options]) if options.key? :options end + + results.all? end failure_message_for_should do |actual| @@ -39,4 +31,26 @@ RSpec::Matchers.define :have_select2 do |id, options={}| match_for_should_not do |node| raise "Not yet implemented" end + + + def all_options_present(from, options) + with_select2_open(from) do + options.all? do |option| + @node.has_selector? "div.select2-drop-active ul.select2-results li", text: option + end + end + end + + def exact_options_present(from, options) + with_select2_open(from) do + @node.all("div.select2-drop-active ul.select2-results li").map(&:text) == options + end + end + + def with_select2_open(from) + find(from).click + r = yield + find(from).click + r + end end From 912c60f720d8e296307b8e35e855ceb0acc79ddf Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 20 Nov 2014 10:06:23 +1100 Subject: [PATCH 263/681] Do not show producers in hubs list --- .../override_variants_controller.js.coffee | 4 +-- .../admin/products_controller_decorator.rb | 3 +- app/helpers/admin/injection_helper.rb | 4 +++ app/helpers/order_cycles_helper.rb | 32 +++++++++++-------- .../override_variants/_data.html.haml | 3 +- .../override_variants/_hub_choice.html.haml | 2 +- spec/features/admin/override_variants_spec.rb | 3 +- 7 files changed, 30 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index a2de809d8c..980256f90f 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,2 +1,2 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Enterprises) -> - $scope.Enterprises = Enterprises +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, hubs) -> + $scope.hubs = hubs diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 25b25c6b8f..1bf90fb74a 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -50,8 +50,7 @@ Spree::Admin::ProductsController.class_eval do end def override_variants - @all_enterprises = [] - @my_enterprises = order_cycle_permitted_enterprises.by_name + @hubs = order_cycle_hub_enterprises(without_validation: true) end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 7db604ba5f..3d0b68f450 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -25,6 +25,10 @@ module Admin admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer end + def admin_inject_hubs + admin_inject_json_ams_array "ofn.admin", "hubs", @hubs, Api::Admin::IdNameSerializer + end + def admin_inject_producers admin_inject_json_ams_array "ofn.admin", "producers", @producers, Api::Admin::IdNameSerializer end diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 2d230f19d2..3868de95c0 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -15,23 +15,27 @@ module OrderCyclesHelper order_cycle_permitted_enterprises.is_distributor.by_name end - def order_cycle_hub_enterprises + def order_cycle_hub_enterprises(options={}) enterprises = order_cycle_permitted_enterprises.is_distributor.by_name - enterprises.map do |e| - disabled_message = nil - if e.shipping_methods.empty? && e.payment_methods.available.empty? - disabled_message = 'no shipping or payment methods' - elsif e.shipping_methods.empty? - disabled_message = 'no shipping methods' - elsif e.payment_methods.available.empty? - disabled_message = 'no payment methods' - end + if options[:without_validation] + enterprises + else + enterprises.map do |e| + disabled_message = nil + if e.shipping_methods.empty? && e.payment_methods.available.empty? + disabled_message = 'no shipping or payment methods' + elsif e.shipping_methods.empty? + disabled_message = 'no shipping methods' + elsif e.payment_methods.available.empty? + disabled_message = 'no payment methods' + end - if disabled_message - ["#{e.name} (#{disabled_message})", e.id, {disabled: true}] - else - [e.name, e.id] + if disabled_message + ["#{e.name} (#{disabled_message})", e.id, {disabled: true}] + else + [e.name, e.id] + end end end end diff --git a/app/views/spree/admin/products/override_variants/_data.html.haml b/app/views/spree/admin/products/override_variants/_data.html.haml index 6ff3621ab4..cc615dc3cd 100644 --- a/app/views/spree/admin/products/override_variants/_data.html.haml +++ b/app/views/spree/admin/products/override_variants/_data.html.haml @@ -1 +1,2 @@ -= admin_inject_enterprises += admin_inject_hubs + diff --git a/app/views/spree/admin/products/override_variants/_hub_choice.html.haml b/app/views/spree/admin/products/override_variants/_hub_choice.html.haml index a2519b7312..aa0f7ab738 100644 --- a/app/views/spree/admin/products/override_variants/_hub_choice.html.haml +++ b/app/views/spree/admin/products/override_variants/_hub_choice.html.haml @@ -2,6 +2,6 @@ .two.columns.alpha Hub .four.columns - %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', 'ng-options' => 'hub.id as hub.name for hub in Enterprises.my_enterprises' } + %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', 'ng-options' => 'hub.id as hub.name for hub in hubs' } .ten.columns.omega %input{ type: 'button', value: 'Go', 'ng-click' => 'selectHub()' } diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb index 5df70b200b..e67eae34ce 100644 --- a/spec/features/admin/override_variants_spec.rb +++ b/spec/features/admin/override_variants_spec.rb @@ -17,10 +17,11 @@ feature %q{ describe "selecting a hub" do let!(:hub) { create(:distributor_enterprise) } + let!(:producer) { create(:supplier_enterprise) } it "displays a list of hub choices" do visit '/admin/products/override_variants' - page.should have_select2 'hub_id', with_options: [hub.name] + page.should have_select2 'hub_id', options: ['', hub.name] end end end From 680ba379c10c666d54042ed3c5c388b5d012bfc3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 20 Nov 2014 10:29:48 +1100 Subject: [PATCH 264/681] User can select a hub --- .../override_variants_controller.js.coffee | 4 +++ .../products/override_variants.html.haml | 2 ++ spec/features/admin/override_variants_spec.rb | 8 ++++++ ...verride_variants_controller_spec.js.coffee | 26 +++++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 980256f90f..69bc5fb8cd 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,2 +1,6 @@ angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, hubs) -> $scope.hubs = hubs + $scope.hub = null + + $scope.selectHub = -> + $scope.hub = (hub for hub in hubs when hub.id == $scope.hub_id)[0] \ No newline at end of file diff --git a/app/views/spree/admin/products/override_variants.html.haml b/app/views/spree/admin/products/override_variants.html.haml index 6f9ffafb31..51c6e6d0d4 100644 --- a/app/views/spree/admin/products/override_variants.html.haml +++ b/app/views/spree/admin/products/override_variants.html.haml @@ -3,3 +3,5 @@ %div{ ng: { app: 'ofn.admin', controller: 'AdminOverrideVariantsCtrl', init: 'initialise()' } } = render 'spree/admin/products/override_variants/hub_choice' + + %h2{ng: {show: 'hub'}} {{ hub.name }} diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb index e67eae34ce..0e84321a97 100644 --- a/spec/features/admin/override_variants_spec.rb +++ b/spec/features/admin/override_variants_spec.rb @@ -23,5 +23,13 @@ feature %q{ visit '/admin/products/override_variants' page.should have_select2 'hub_id', options: ['', hub.name] end + + it "displays the hub" do + visit '/admin/products/override_variants' + select2_select hub.name, from: 'hub_id' + click_button 'Go' + + page.should have_selector 'h2', text: hub.name + end end end diff --git a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee new file mode 100644 index 0000000000..6a3d895256 --- /dev/null +++ b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee @@ -0,0 +1,26 @@ +describe "OverrideVariantsCtrl", -> + ctrl = null + scope = null + hubs = [{id: 1, name: 'Hub'}] + + beforeEach -> + module 'ofn.admin' + scope = {} + + inject ($controller)-> + ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, hubs: hubs} + + it "initialises the hub list and the chosen hub", -> + expect(scope.hubs).toEqual hubs + expect(scope.hub).toBeNull + + describe "selecting a hub", -> + it "sets the chosen hub", -> + scope.hub_id = 1 + scope.selectHub() + expect(scope.hub).toEqual hubs[0] + + it "does nothing when no selection has been made", -> + scope.hub_id = '' + scope.selectHub + expect(scope.hub).toBeNull \ No newline at end of file From 7069b30e71fdb817d87b4d1fb8c94123ed42c0cb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 20 Nov 2014 12:25:24 +1100 Subject: [PATCH 265/681] Add indexer service (equivalent of Dereferencer, but named more accurately) --- .../javascripts/admin/services/indexer.js.coffee | 13 +++++++++++++ .../unit/admin/services/indexer_spec.js.coffee | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 app/assets/javascripts/admin/services/indexer.js.coffee create mode 100644 spec/javascripts/unit/admin/services/indexer_spec.js.coffee diff --git a/app/assets/javascripts/admin/services/indexer.js.coffee b/app/assets/javascripts/admin/services/indexer.js.coffee new file mode 100644 index 0000000000..a69f7f06e1 --- /dev/null +++ b/app/assets/javascripts/admin/services/indexer.js.coffee @@ -0,0 +1,13 @@ +# Convert an array of objects into a hash, indexed by the objects' ids +# +# producers = [{id: 1, name: 'one'}, {id: 2, name: 'two'}] +# Indexer.index producers +# -> {1: {id: 1, name: 'one'}, 2: {id: 2, name: 'two'}} + +angular.module("ofn.admin").factory 'Indexer', -> + new class Indexer + index: (data) -> + index = [] + for e in data + index[e.id] = e + index diff --git a/spec/javascripts/unit/admin/services/indexer_spec.js.coffee b/spec/javascripts/unit/admin/services/indexer_spec.js.coffee new file mode 100644 index 0000000000..a7f009e7d3 --- /dev/null +++ b/spec/javascripts/unit/admin/services/indexer_spec.js.coffee @@ -0,0 +1,13 @@ +describe "indexer", -> + Indexer = null + + beforeEach -> + module "ofn.admin" + + beforeEach inject (_Indexer_) -> + Indexer = _Indexer_ + + it "indexes an array of objects by id", -> + objects = [{id: 1, name: 'one'}, {id: 2, name: 'two'}] + index = Indexer.index objects + expect(index).toEqual({1: {id: 1, name: 'one'}, 2: {id: 2, name: 'two'}}) From 58fdc48b9f5927e3e0115446833b73532bf794b0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 20 Nov 2014 12:28:26 +1100 Subject: [PATCH 266/681] Display products in table --- .../override_variants_controller.js.coffee | 4 +++- .../admin/products_controller_decorator.rb | 2 ++ app/helpers/admin/injection_helper.rb | 4 ++++ app/models/spree/product_decorator.rb | 3 +++ .../products/override_variants.html.haml | 2 ++ .../override_variants/_data.html.haml | 3 ++- .../override_variants/_products.html.haml | 13 +++++++++++ spec/features/admin/override_variants_spec.rb | 23 ++++++++++++++++--- ...verride_variants_controller_spec.js.coffee | 6 +++-- 9 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 app/views/spree/admin/products/override_variants/_products.html.haml diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 69bc5fb8cd..65426a0422 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,6 +1,8 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, hubs) -> +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, hubs, producers, products) -> $scope.hubs = hubs $scope.hub = null + $scope.products = products + $scope.producers = Indexer.index producers $scope.selectHub = -> $scope.hub = (hub for hub in hubs when hub.id == $scope.hub_id)[0] \ No newline at end of file diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 1bf90fb74a..cfd4649fc1 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -51,6 +51,8 @@ Spree::Admin::ProductsController.class_eval do def override_variants @hubs = order_cycle_hub_enterprises(without_validation: true) + @producers = order_cycle_producer_enterprises + @products = Spree::Product.not_deleted.where(supplier_id: @producers).by_producer.by_name end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 3d0b68f450..94ee23347a 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -33,6 +33,10 @@ module Admin admin_inject_json_ams_array "ofn.admin", "producers", @producers, Api::Admin::IdNameSerializer end + def admin_inject_products + admin_inject_json_ams_array "ofn.admin", "products", @products, Spree::Api::ProductSerializer + end + def admin_inject_taxons admin_inject_json_ams_array "ofn.admin", "taxons", @taxons, Api::Admin::TaxonSerializer end diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 5181bf776d..5b3460a073 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -87,6 +87,9 @@ Spree::Product.class_eval do merge(Exchange.outgoing). where('order_cycles.id IS NOT NULL') } + scope :by_producer, joins(:supplier).order('enterprises.name') + scope :by_name, order('name') + scope :managed_by, lambda { |user| if user.has_spree_role?('admin') scoped diff --git a/app/views/spree/admin/products/override_variants.html.haml b/app/views/spree/admin/products/override_variants.html.haml index 51c6e6d0d4..a3165f8850 100644 --- a/app/views/spree/admin/products/override_variants.html.haml +++ b/app/views/spree/admin/products/override_variants.html.haml @@ -5,3 +5,5 @@ = render 'spree/admin/products/override_variants/hub_choice' %h2{ng: {show: 'hub'}} {{ hub.name }} + + = render 'spree/admin/products/override_variants/products' diff --git a/app/views/spree/admin/products/override_variants/_data.html.haml b/app/views/spree/admin/products/override_variants/_data.html.haml index cc615dc3cd..09d8aca54e 100644 --- a/app/views/spree/admin/products/override_variants/_data.html.haml +++ b/app/views/spree/admin/products/override_variants/_data.html.haml @@ -1,2 +1,3 @@ = admin_inject_hubs - += admin_inject_producers += admin_inject_products diff --git a/app/views/spree/admin/products/override_variants/_products.html.haml b/app/views/spree/admin/products/override_variants/_products.html.haml new file mode 100644 index 0000000000..a5abae6ba1 --- /dev/null +++ b/app/views/spree/admin/products/override_variants/_products.html.haml @@ -0,0 +1,13 @@ +%table.index.bulk{ng: {show: 'hub'}} + %thead + %tr + %th Producer + %th Product + %th Price + %th On hand + %tbody + %tr{ng: {repeat: 'product in products'}} + %td {{ producers[product.producer].name }} + %td {{ product.name }} + %td {{ product.price }} + %td {{ product.on_hand }} diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb index 0e84321a97..0372fed47d 100644 --- a/spec/features/admin/override_variants_spec.rb +++ b/spec/features/admin/override_variants_spec.rb @@ -15,10 +15,10 @@ feature %q{ use_short_wait - describe "selecting a hub" do - let!(:hub) { create(:distributor_enterprise) } - let!(:producer) { create(:supplier_enterprise) } + let!(:hub) { create(:distributor_enterprise) } + let!(:producer) { create(:supplier_enterprise) } + describe "selecting a hub" do it "displays a list of hub choices" do visit '/admin/products/override_variants' page.should have_select2 'hub_id', options: ['', hub.name] @@ -32,4 +32,21 @@ feature %q{ page.should have_selector 'h2', text: hub.name end end + + context "when a hub is selected" do + let!(:product) { create(:simple_product, supplier: producer, price: 1.23, on_hand: 12) } + + before do + visit '/admin/products/override_variants' + select2_select hub.name, from: 'hub_id' + click_button 'Go' + end + + it "displays the list of products" do + page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] + page.should have_table_row [producer.name, product.name, '1.23', '12'] + end + + it "products values are affected by overrides" + end end diff --git a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee index 6a3d895256..8c5144454b 100644 --- a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee @@ -2,13 +2,15 @@ describe "OverrideVariantsCtrl", -> ctrl = null scope = null hubs = [{id: 1, name: 'Hub'}] + producers = [{id: 2, name: 'Producer'}] + products = [{id: 1, name: 'Product'}] beforeEach -> module 'ofn.admin' scope = {} - inject ($controller)-> - ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, hubs: hubs} + inject ($controller, Indexer) -> + ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products} it "initialises the hub list and the chosen hub", -> expect(scope.hubs).toEqual hubs From 824b00743e91a5e93bb691a1f10efbe3cb55dfb8 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 21 Nov 2014 09:56:29 +1100 Subject: [PATCH 267/681] Extract fetch-by-page from BPE to service --- .../admin/services/bulk_products.js.coffee | 10 +++------- .../admin/services/paged_fetcher.js.coffee | 16 ++++++++++++++++ .../unit/admin/services/paged_fetcher.js.coffee | 13 +++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/admin/services/paged_fetcher.js.coffee create mode 100644 spec/javascripts/unit/admin/services/paged_fetcher.js.coffee diff --git a/app/assets/javascripts/admin/services/bulk_products.js.coffee b/app/assets/javascripts/admin/services/bulk_products.js.coffee index ae845c23b9..d898356846 100644 --- a/app/assets/javascripts/admin/services/bulk_products.js.coffee +++ b/app/assets/javascripts/admin/services/bulk_products.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").factory "BulkProducts", (dataFetcher) -> +angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher) -> new class BulkProducts products: [] @@ -6,13 +6,9 @@ angular.module("ofn.admin").factory "BulkProducts", (dataFetcher) -> queryString = filters.reduce (qs,f) -> return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};" , "" - return dataFetcher("/api/products/bulk_products?page=1;per_page=20;#{queryString}").then (data) => - @addProducts data.products - if data.pages > 1 - for page in [2..data.pages] - dataFetcher("/api/products/bulk_products?page=#{page};per_page=20;#{queryString}").then (data) => - @addProducts data.products + url = "/api/products/bulk_products?page=::page::;per_page=20;#{queryString}" + PagedFetcher.fetch url, (data) => @addProducts data.products cloneProduct: (product) -> dataFetcher("/admin/products/" + product.permalink_live + "/clone.json").then (data) => diff --git a/app/assets/javascripts/admin/services/paged_fetcher.js.coffee b/app/assets/javascripts/admin/services/paged_fetcher.js.coffee new file mode 100644 index 0000000000..9281ed6a42 --- /dev/null +++ b/app/assets/javascripts/admin/services/paged_fetcher.js.coffee @@ -0,0 +1,16 @@ +angular.module("ofn.admin").factory "PagedFetcher", (dataFetcher) -> + new class PagedFetcher + # Given a URL like http://example.com/foo?page=::page::&per_page=20 + # And the response includes an attribute pages with the number of pages to fetch + # Fetch each page async, and call the processData callback with the resulting data + fetch: (url, processData) -> + dataFetcher(@urlForPage(url, 1)).then (data) => + processData data + + if data.pages > 1 + for page in [2..data.pages] + dataFetcher(@urlForPage(url, page)).then (data) -> + processData data + + urlForPage: (url, page) -> + url.replace("::page::", page) \ No newline at end of file diff --git a/spec/javascripts/unit/admin/services/paged_fetcher.js.coffee b/spec/javascripts/unit/admin/services/paged_fetcher.js.coffee new file mode 100644 index 0000000000..b6c2fdf9e1 --- /dev/null +++ b/spec/javascripts/unit/admin/services/paged_fetcher.js.coffee @@ -0,0 +1,13 @@ +describe "PagedFetcher service", -> + PagedFetcher = null + + beforeEach -> + module "ofn.admin" + + beforeEach inject (_PagedFetcher_) -> + PagedFetcher = _PagedFetcher_ + + describe "substituting a page in the URL", -> + it "replaces ::page:: with the given page number", -> + expect(PagedFetcher.urlForPage("http://example.com/foo?page=::page::&per_page=20", 12)). + toEqual "http://example.com/foo?page=12&per_page=20" From 001bf999d038a091f4373feddcf8687a2fd3b10c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 21 Nov 2014 10:59:15 +1100 Subject: [PATCH 268/681] Extract Spree API authorisation to service --- .../admin/bulk_product_update.js.coffee | 19 +++++++------------ .../admin/services/spree_api_auth.js.coffee | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/admin/services/spree_api_auth.js.coffee diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index fdb1e3be13..ddd3bda162 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons, SpreeApiKey) -> +angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons, SpreeApiAuth) -> $scope.loading = true $scope.updateStatusMessage = @@ -45,17 +45,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.DisplayProperties = DisplayProperties $scope.initialise = -> - authorise_api_reponse = "" - dataFetcher("/api/users/authorise_api?token=" + SpreeApiKey).then (data) -> - authorise_api_reponse = data - $scope.spree_api_key_ok = data.hasOwnProperty("success") and data["success"] == "Use of API Authorised" - if $scope.spree_api_key_ok - $http.defaults.headers.common["X-Spree-Token"] = SpreeApiKey - $scope.fetchProducts() - else if authorise_api_reponse.hasOwnProperty("error") - $scope.api_error_msg = authorise_api_reponse("error") - else - api_error_msg = "You don't have an API key yet. An attempt was made to generate one, but you are currently not authorised, please contact your site administrator for access." + SpreeApiAuth.authorise() + .then -> + $scope.spree_api_key_ok = true + $scope.fetchProducts() + .catch (message) -> + $scope.api_error_msg = message $scope.$watchCollection '[query, producerFilter, categoryFilter]', -> $scope.limit = 15 # Reset limit whenever searching diff --git a/app/assets/javascripts/admin/services/spree_api_auth.js.coffee b/app/assets/javascripts/admin/services/spree_api_auth.js.coffee new file mode 100644 index 0000000000..e606882bc5 --- /dev/null +++ b/app/assets/javascripts/admin/services/spree_api_auth.js.coffee @@ -0,0 +1,16 @@ +angular.module("ofn.admin").factory "SpreeApiAuth", ($q, $http, SpreeApiKey) -> + new class SpreeApiAuth + authorise: -> + deferred = $q.defer() + + $http.get("/api/users/authorise_api?token=" + SpreeApiKey) + .success (response) -> + if response?.success == "Use of API Authorised" + $http.defaults.headers.common["X-Spree-Token"] = SpreeApiKey + deferred.resolve() + + .error (response) -> + error = response?.error || "You are unauthorised to access this page." + deferred.reject(error) + + deferred.promise From 0b030a85ff375675447e35f7cfb9052eb9caba75 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 21 Nov 2014 12:44:59 +1100 Subject: [PATCH 269/681] Incrementally load products --- .../override_variants_controller.js.coffee | 25 ++++++++++++++++--- .../admin/products_controller_decorator.rb | 3 +-- .../api/products_controller_decorator.rb | 15 +++++++++++ .../override_variants/_data.html.haml | 2 +- .../override_variants/_products.html.haml | 2 +- config/routes.rb | 7 ++++-- ...verride_variants_controller_spec.js.coffee | 10 ++++++++ 7 files changed, 55 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 65426a0422..871cbb9b03 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,8 +1,27 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, hubs, producers, products) -> +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, hubs, producers) -> $scope.hubs = hubs $scope.hub = null - $scope.products = products + $scope.products = [] $scope.producers = Indexer.index producers + + $scope.initialise = -> + SpreeApiAuth.authorise() + .then -> + $scope.spree_api_key_ok = true + $scope.fetchProducts() + .catch (message) -> + $scope.api_error_msg = message + + + $scope.fetchProducts = -> + url = "/api/products/distributable?page=::page::;per_page=100" + PagedFetcher.fetch url, (data) => $scope.addProducts data.products + + + $scope.addProducts = (products) -> + $scope.products = $scope.products.concat products + + $scope.selectHub = -> - $scope.hub = (hub for hub in hubs when hub.id == $scope.hub_id)[0] \ No newline at end of file + $scope.hub = (hub for hub in hubs when hub.id == $scope.hub_id)[0] diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index cfd4649fc1..d44df1ffb5 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -4,7 +4,7 @@ Spree::Admin::ProductsController.class_eval do include OpenFoodNetwork::SpreeApiKeyLoader include OrderCyclesHelper before_filter :load_form_data, :only => [:bulk_edit, :new, :create, :edit, :update] - before_filter :load_spree_api_key, :only => :bulk_edit + before_filter :load_spree_api_key, :only => [:bulk_edit, :override_variants] alias_method :location_after_save_original, :location_after_save @@ -52,7 +52,6 @@ Spree::Admin::ProductsController.class_eval do def override_variants @hubs = order_cycle_hub_enterprises(without_validation: true) @producers = order_cycle_producer_enterprises - @products = Spree::Product.not_deleted.where(supplier_id: @producers).by_producer.by_name end diff --git a/app/controllers/spree/api/products_controller_decorator.rb b/app/controllers/spree/api/products_controller_decorator.rb index 111f36962c..733ce365a2 100644 --- a/app/controllers/spree/api/products_controller_decorator.rb +++ b/app/controllers/spree/api/products_controller_decorator.rb @@ -9,6 +9,7 @@ Spree::Api::ProductsController.class_eval do respond_with(@products, default_template: :index) end + # TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it. def bulk_products @products = OpenFoodNetwork::Permissions.new(current_api_user).managed_products. merge(product_scope). @@ -19,6 +20,20 @@ Spree::Api::ProductsController.class_eval do render text: { products: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer), pages: @products.num_pages }.to_json end + def distributable + producers = OpenFoodNetwork::Permissions.new(current_api_user). + order_cycle_enterprises.is_primary_producer.by_name + + @products = Spree::Product.scoped. + merge(product_scope). + where(supplier_id: producers). + by_producer.by_name. + ransack(params[:q]).result. + page(params[:page]).per(params[:per_page]) + + render text: { products: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer), pages: @products.num_pages }.to_json + end + def soft_delete authorize! :delete, Spree::Product @product = find_product(params[:product_id]) diff --git a/app/views/spree/admin/products/override_variants/_data.html.haml b/app/views/spree/admin/products/override_variants/_data.html.haml index 09d8aca54e..d96db0edb9 100644 --- a/app/views/spree/admin/products/override_variants/_data.html.haml +++ b/app/views/spree/admin/products/override_variants/_data.html.haml @@ -1,3 +1,3 @@ += admin_inject_spree_api_key = admin_inject_hubs = admin_inject_producers -= admin_inject_products diff --git a/app/views/spree/admin/products/override_variants/_products.html.haml b/app/views/spree/admin/products/override_variants/_products.html.haml index a5abae6ba1..f5081e54bd 100644 --- a/app/views/spree/admin/products/override_variants/_products.html.haml +++ b/app/views/spree/admin/products/override_variants/_products.html.haml @@ -7,7 +7,7 @@ %th On hand %tbody %tr{ng: {repeat: 'product in products'}} - %td {{ producers[product.producer].name }} + %td {{ producers[product.producer_id].name }} %td {{ product.name }} %td {{ product.price }} %td {{ product.on_hand }} diff --git a/config/routes.rb b/config/routes.rb index 75380a55f5..fd891fbb9c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -132,8 +132,11 @@ Spree::Core::Engine.routes.prepend do end resources :products do - get :managed, on: :collection - get :bulk_products, on: :collection + collection do + get :managed + get :bulk_products + get :distributable + end delete :soft_delete resources :variants do diff --git a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee index 8c5144454b..f3c796cd51 100644 --- a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee @@ -7,6 +7,9 @@ describe "OverrideVariantsCtrl", -> beforeEach -> module 'ofn.admin' + module ($provide) -> + $provide.value 'SpreeApiKey', 'API_KEY' + null scope = {} inject ($controller, Indexer) -> @@ -16,6 +19,13 @@ describe "OverrideVariantsCtrl", -> expect(scope.hubs).toEqual hubs expect(scope.hub).toBeNull + it "adds products", -> + expect(scope.products).toEqual [] + scope.addProducts ['a', 'b'] + expect(scope.products).toEqual ['a', 'b'] + scope.addProducts ['c', 'd'] + expect(scope.products).toEqual ['a', 'b', 'c', 'd'] + describe "selecting a hub", -> it "sets the chosen hub", -> scope.hub_id = 1 From 49d5cb2f05bc016108ae719df604dbea43bcd88b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 21 Nov 2014 13:47:51 +1100 Subject: [PATCH 270/681] Do not show Override Variants tab - hide this until feature is ready --- .../add_override_variants_tab.html.haml.deface | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface b/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface index 5b4bb2983e..381f80a44d 100644 --- a/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface +++ b/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface @@ -1,3 +1,4 @@ / insert_bottom "[data-hook='admin_product_sub_tabs']" -= tab :override_details, url: override_variants_admin_products_path, match_path: '/products/override_variants' +-# Commented out until this feature is ready +-#= tab :override_details, url: override_variants_admin_products_path, match_path: '/products/override_variants' From 2f28312f7eb778c332402500e70505b9c7f11152 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 21 Nov 2014 14:18:22 +1100 Subject: [PATCH 271/681] Removing incorrectly placed directive in profile shops skinny view --- app/views/home/_skinny.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/home/_skinny.html.haml b/app/views/home/_skinny.html.haml index 7100a06974..aeaae0cae4 100644 --- a/app/views/home/_skinny.html.haml +++ b/app/views/home/_skinny.html.haml @@ -26,7 +26,7 @@ .row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed"} .columns.small-12.medium-6.large-5.skinny-head - %a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"} + %a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}"} %i{ng: {class: "hub.icon_font"}} %span.margin-top.hub-name-listing {{ hub.name | truncate:40}} @@ -36,5 +36,5 @@ %span.margin-top {{ hub.address.state_name | uppercase }} .columns.small-6.medium-3.large-4.text-right - %span.margin-top{ bo: { if: "!current()" } } + %span.margin-top{ bo: { if: "!current()" } } %em Profile only From c3829ae64f587bc6f4f57f745730a64ae9c564b7 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley Date: Sun, 23 Nov 2014 15:18:16 +0000 Subject: [PATCH 272/681] Tax category dropdown on create product form --- .../spree/app_configuration_decorator.rb | 9 ++++++++ app/models/spree/product_decorator.rb | 3 ++- .../new/replace_form.html.haml.deface | 2 ++ ...ucts_require_tax_category.html.haml.deface | 6 +++++ spec/factories.rb | 5 +++++ spec/features/admin/products_spec.rb | 5 +++++ spec/models/spree/product_spec.rb | 22 +++++++++++++++++++ 7 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 app/models/spree/app_configuration_decorator.rb create mode 100644 app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb new file mode 100644 index 0000000000..f4f4acc5ec --- /dev/null +++ b/app/models/spree/app_configuration_decorator.rb @@ -0,0 +1,9 @@ +Spree::AppConfiguration.class_eval do + # This file decorates the existing preferences file defined by Spree. + # It allows us to add our own global configuration variables, which + # we can allow to be modified in the UI by adding appropriate form + # elements to existing or new configuration pages. + + # Tax Preferences + preference :products_require_tax_category, :boolean, default: false +end \ No newline at end of file diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 5b3460a073..9d7eb18d80 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -20,7 +20,8 @@ Spree::Product.class_eval do validates_associated :master, message: "^Price and On Hand must be valid" validates_presence_of :supplier validates :primary_taxon, presence: { message: "^Product Category can't be blank" } - + validates :tax_category_id, presence: { message: "^Tax Category can't be blank" }, if: "Spree::Config.products_require_tax_category" + validates_presence_of :variant_unit, if: :has_variants? validates_presence_of :variant_unit_scale, if: -> p { %w(weight volume).include? p.variant_unit } diff --git a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface index 7c627eec4e..974e31f3ee 100644 --- a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface +++ b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface @@ -38,6 +38,8 @@ .twelve.columns.alpha .six.columns.alpha = render 'spree/admin/products/primary_taxon_form', f: f + - if Spree::TaxCategory.any? + = render 'spree/admin/products/tax_category_form', f: f .three.columns = f.field_container :price do = f.label :price, t(:price) diff --git a/app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface b/app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface new file mode 100644 index 0000000000..588669f005 --- /dev/null +++ b/app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface @@ -0,0 +1,6 @@ +/ insert_before "[data-hook='shipment_vat']" + +%div.field.align-center{ "data-hook" => "products_require_tax_category" } + = hidden_field_tag 'preferences[products_require_tax_category]', '0' + = check_box_tag 'preferences[products_require_tax_category]', '1', Spree::Config[:products_require_tax_category] + = label_tag nil, t(:products_require_tax_category) \ No newline at end of file diff --git a/spec/factories.rb b/spec/factories.rb index 0112ea8b51..6f86f430d4 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -175,6 +175,11 @@ FactoryGirl.define do order.reload end end + + factory :simple_tax_category, :class => Spree::TaxCategory do + name "Test Tax Category" + description "Test tax category description" + end end diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index 1f61b07d2a..c42a57ce98 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -16,11 +16,13 @@ feature %q{ describe "creating a product" do scenario "assigning a important attributes", js: true do + tax_category = create(:simple_tax_category) login_to_admin_section click_link 'Products' click_link 'New Product' + select 'Test Tax Category', from: 'product_tax_category_id' select 'New supplier', from: 'product_supplier_id' fill_in 'product_name', with: 'A new product !!!' select "Weight (kg)", from: 'product_variant_unit_with_scale' @@ -34,6 +36,7 @@ feature %q{ flash_message.should == 'Product "A new product !!!" has been successfully created!' product = Spree::Product.find_by_name('A new product !!!') + product.tax_category_id.should == tax_category.id product.supplier.should == @supplier product.variant_unit.should == 'weight' product.variant_unit_scale.should == 1000 @@ -117,6 +120,7 @@ feature %q{ page.should have_selector('#product_supplier_id') select 'Another Supplier', :from => 'product_supplier_id' select taxon.name, from: "product_primary_taxon_id" + page.should have_select 'product_tax_category_id' if Spree::TaxCategory.any? # Should only have suppliers listed which the user can manage page.should have_select 'product_supplier_id', with_options: [@supplier2.name, @supplier_permitted.name] @@ -134,6 +138,7 @@ feature %q{ visit spree.edit_admin_product_path product + page.should have_select 'product_tax_category_id' select 'Permitted Supplier', from: 'product_supplier_id' click_button 'Update' flash_message.should == 'Product "a product" has been successfully updated!' diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index c6acae0850..f40e00f856 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -8,6 +8,28 @@ module Spree it { should belong_to(:primary_taxon) } it { should have_many(:product_distributions) } end + + describe "validating tax category" do + context "when a tax category is required" do + before { Spree::Config.products_require_tax_category = true } + + it "is invalid when a tax category is not provided" do + product = create(:product) + product.tax_category_id = nil + product.should_not be_valid + end + end + + context "when a tax category is not required" do + before { Spree::Config.products_require_tax_category = false } + + it "is valid when a tax category is not provided" do + product = create(:product) + product.tax_category_id = nil + product.should be_valid + end + end + end describe "validations and defaults" do it "is valid when created from factory" do From 451dd3966f4d69efb7ffb444ec9e8427c7fb1ef6 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley Date: Sun, 23 Nov 2014 15:22:56 +0000 Subject: [PATCH 273/681] form partial --- app/views/spree/admin/products/_tax_category_form.html.haml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 app/views/spree/admin/products/_tax_category_form.html.haml diff --git a/app/views/spree/admin/products/_tax_category_form.html.haml b/app/views/spree/admin/products/_tax_category_form.html.haml new file mode 100644 index 0000000000..9ab348636c --- /dev/null +++ b/app/views/spree/admin/products/_tax_category_form.html.haml @@ -0,0 +1,6 @@ += f.field_container :tax_category_id do + = f.label :tax_category_id, t(:tax_category) + * + %br + = f.collection_select(:tax_category_id, Spree::TaxCategory.all, :id, :name, {:include_blank => Spree::TaxCategory.count > 1}, {:class => "select2 fullwidth"}) + = f.error_message_on :tax_category_id From 4f153714a8710c71744d5f1d1dd8bbe29e5481c5 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley Date: Sun, 23 Nov 2014 17:26:26 +0000 Subject: [PATCH 274/681] Reports pages improvements and i18n --- .../admin/reports_controller_decorator.rb | 14 ++++---- .../spree/admin/reports/bulk_coop.html.haml | 9 +++-- .../spree/admin/reports/customers.html.haml | 31 +++++++++-------- .../spree/admin/reports/payments.html.haml | 9 +++-- .../reports/products_and_inventory.html.haml | 34 +++++++++++-------- 5 files changed, 55 insertions(+), 42 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 622edbe9a9..5a5f2414ef 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -257,12 +257,14 @@ Spree::Admin::ReportsController.class_eval do @distributors = Enterprise.is_distributor.managed_by(spree_current_user) @report_type = params[:report_type] - + + currency_symbol = Money.new(1, Spree::Config[:currency]).symbol + case params[:report_type] when "payments_by_payment_type" table_items = payments - header = ["Payment State", "Distributor", "Payment Type", "Total ($)"] + header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] columns = [ proc { |payments| payments.first.order.payment_state }, proc { |payments| payments.first.order.distributor.name }, @@ -279,7 +281,7 @@ Spree::Admin::ReportsController.class_eval do when "itemised_payment_totals" table_items = orders - header = ["Payment State", "Distributor", "Product Total ($)", "Shipping Total ($)", "Outstanding Balance ($)", "Total ($)"] + header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})", "Total (#{currency_symbol})"] columns = [ proc { |orders| orders.first.payment_state }, proc { |orders| orders.first.distributor.name }, @@ -296,7 +298,7 @@ Spree::Admin::ReportsController.class_eval do when "payment_totals" table_items = orders - header = ["Payment State", "Distributor", "Product Total ($)", "Shipping Total ($)", "Total ($)", "EFT ($)", "PayPal ($)", "Outstanding Balance ($)"] + header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Total (#{currency_symbol})", "EFT (#{currency_symbol})", "PayPal (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})"] columns = [ proc { |orders| orders.first.payment_state }, proc { |orders| orders.first.distributor.name }, @@ -315,7 +317,7 @@ Spree::Admin::ReportsController.class_eval do else table_items = payments - header = ["Payment State", "Distributor", "Payment Type", "Total ($)"] + header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] columns = [ proc { |payments| payments.first.order.payment_state }, proc { |payments| payments.first.order.distributor.name }, @@ -475,7 +477,7 @@ Spree::Admin::ReportsController.class_eval do table_items = @line_items @include_blank = 'All' - header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item ($)", "Dist ($)", "Ship ($)", "Total ($)", "Paid?", + header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item (#{currency_symbol})", "Dist (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", "Shipping", "Delivery?", "Ship street", "Ship street 2", "Ship city", "Ship postcode", "Ship state", "Order notes"] rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address } diff --git a/app/views/spree/admin/reports/bulk_coop.html.haml b/app/views/spree/admin/reports/bulk_coop.html.haml index adde173e98..c405b78526 100644 --- a/app/views/spree/admin/reports/bulk_coop.html.haml +++ b/app/views/spree/admin/reports/bulk_coop.html.haml @@ -11,16 +11,19 @@ %br = label_tag nil, t(:stop) %br - = label_tag nil, "Distributor: " - = f.collection_select(:distributor_id_eq, @distributors, :id, :name, :include_blank => 'All') - %br + %div{"class" => "row"} + %div{"class" => "four columns alpha"} + = label_tag nil, "Distributor: " + = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) = label_tag nil, "Report Type: " + %br = select_tag(:report_type, options_for_select([['Bulk Co-op - Totals by Supplier',:bulk_coop_supplier_report],['Bulk Co-op - Allocation',:bulk_coop_allocation],['Bulk Co-op - Packing Sheets',:bulk_coop_packing_sheets],['Bulk Co-op - Customer Payments',:bulk_coop_customer_payments]], @report_type)) %br %br = check_box_tag :csv = label_tag :csv, "Download as csv" %br + %br = button t(:search) %br %br diff --git a/app/views/spree/admin/reports/customers.html.haml b/app/views/spree/admin/reports/customers.html.haml index ebe1d31492..db5fc94ce6 100644 --- a/app/views/spree/admin/reports/customers.html.haml +++ b/app/views/spree/admin/reports/customers.html.haml @@ -1,23 +1,24 @@ = form_tag spree.customers_admin_reports_url do |f| %br - = label_tag nil, "Distributor: " - = select_tag(:distributor_id, - options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), - :include_blank => true) + %div{"class" => "row"} + %div{"class" => "four columns alpha"} + = label_tag nil, "Distributor: " + = select_tag(:distributor_id, + options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), + {:include_blank => true, :class => "select2 fullwidth"}) - %br - = label_tag nil, "Supplier: " - = select_tag(:supplier_id, - options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), - :include_blank => true) + %div{"class" => "four columns"} + = label_tag nil, "Supplier: " + = select_tag(:supplier_id, + options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), + {:include_blank => true, :class => "select2 fullwidth"}) - %br - = label_tag nil, "Order Cycle: " - = select_tag(:order_cycle_id, - options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), - include_blank: true) + %div{"class" => "six columns"} + = label_tag nil, "Order Cycle: " + = select_tag(:order_cycle_id, + options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), + {:include_blank => true, :class => "select2 fullwidth"}) - %br = label_tag nil, "Report Type: " = select_tag(:report_type, options_for_select(@report_types, @report_type)) diff --git a/app/views/spree/admin/reports/payments.html.haml b/app/views/spree/admin/reports/payments.html.haml index 66da52ad4b..9f145e2858 100644 --- a/app/views/spree/admin/reports/payments.html.haml +++ b/app/views/spree/admin/reports/payments.html.haml @@ -11,16 +11,19 @@ %br = label_tag nil, t(:stop) %br - = label_tag nil, "Distributor: " - = f.collection_select(:distributor_id_eq, @distributors, :id, :name, :include_blank => 'All') - %br + %div{"class" => "row"} + %div{"class" => "four columns alpha"} + = label_tag nil, "Distributor: " + = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) = label_tag nil, "Report Type: " + %br = select_tag(:report_type, options_for_select([['Payments By Type',:payments_by_payment_type],['Itemised Payment Totals',:itemised_payment_totals],['Payment Totals',:payment_totals]], @report_type)) %br %br = check_box_tag :csv = label_tag :csv, "Download as csv" %br + %br = button t(:search) %br %br diff --git a/app/views/spree/admin/reports/products_and_inventory.html.haml b/app/views/spree/admin/reports/products_and_inventory.html.haml index f5e0b9ce6b..22bd58c29f 100644 --- a/app/views/spree/admin/reports/products_and_inventory.html.haml +++ b/app/views/spree/admin/reports/products_and_inventory.html.haml @@ -1,24 +1,28 @@ = form_tag spree.products_and_inventory_admin_reports_url do |f| %br - = label_tag nil, "Distributor: " - = select_tag(:distributor_id, - options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), - :include_blank => true) + %div{"class" => "row"} + %div{"class" => "four columns alpha"} + = label_tag nil, "Distributor: " + = select_tag(:distributor_id, + options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), + {:include_blank => true, :class => "select2 fullwidth"}) - %br - = label_tag nil, "Supplier: " - = select_tag(:supplier_id, - options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), - :include_blank => true) + + %div{"class" => "four columns"} + = label_tag nil, "Supplier: " + = select_tag(:supplier_id, + options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), + {:include_blank => true, :class => "select2 fullwidth"}) - %br - = label_tag nil, "Order Cycle: " - = select_tag(:order_cycle_id, - options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), - include_blank: true) + + %div{"class" => "six columns"} + = label_tag nil, "Order Cycle: " + = select_tag(:order_cycle_id, + options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), + {:include_blank => true, :class => "select2 fullwidth"}) - %br = label_tag nil, "Report Type: " + %br = select_tag(:report_type, options_for_select(@report_types, params[:report_type])) %br From e4d1ae7548f1602533ceb3187ebf1bbe04b44890 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley Date: Mon, 24 Nov 2014 18:20:10 +0000 Subject: [PATCH 275/681] Refactored currency symbol display --- .../admin/reports_controller_decorator.rb | 19 +++++++++++-------- app/models/spree/money_decorator.rb | 7 +++++++ 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 app/models/spree/money_decorator.rb diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 5a5f2414ef..fb2848efbe 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -27,6 +27,7 @@ Spree::Admin::ReportsController.class_eval do # Fetches user's distributors, suppliers and order_cycles before_filter :load_data, only: [:customers, :products_and_inventory] + before_filter :set_currency_symbol # Render a partial for orders and fulfillment description respond_override :index => { :html => { :success => lambda { @@ -257,14 +258,12 @@ Spree::Admin::ReportsController.class_eval do @distributors = Enterprise.is_distributor.managed_by(spree_current_user) @report_type = params[:report_type] - - currency_symbol = Money.new(1, Spree::Config[:currency]).symbol - + case params[:report_type] when "payments_by_payment_type" table_items = payments - header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] + header = ["Payment State", "Distributor", "Payment Type", "Total (#{@currency_symbol})"] columns = [ proc { |payments| payments.first.order.payment_state }, proc { |payments| payments.first.order.distributor.name }, @@ -281,7 +280,7 @@ Spree::Admin::ReportsController.class_eval do when "itemised_payment_totals" table_items = orders - header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})", "Total (#{currency_symbol})"] + header = ["Payment State", "Distributor", "Product Total (#{@currency_symbol})", "Shipping Total (#{@currency_symbol})", "Outstanding Balance (#{@currency_symbol})", "Total (#{@currency_symbol})"] columns = [ proc { |orders| orders.first.payment_state }, proc { |orders| orders.first.distributor.name }, @@ -298,7 +297,7 @@ Spree::Admin::ReportsController.class_eval do when "payment_totals" table_items = orders - header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Total (#{currency_symbol})", "EFT (#{currency_symbol})", "PayPal (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})"] + header = ["Payment State", "Distributor", "Product Total (#{@currency_symbol})", "Shipping Total (#{@currency_symbol})", "Total (#{@currency_symbol})", "EFT (#{@currency_symbol})", "PayPal (#{@currency_symbol})", "Outstanding Balance (#{@currency_symbol})"] columns = [ proc { |orders| orders.first.payment_state }, proc { |orders| orders.first.distributor.name }, @@ -317,7 +316,7 @@ Spree::Admin::ReportsController.class_eval do else table_items = payments - header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] + header = ["Payment State", "Distributor", "Payment Type", "Total (#{@currency_symbol})"] columns = [ proc { |payments| payments.first.order.payment_state }, proc { |payments| payments.first.order.distributor.name }, @@ -477,7 +476,7 @@ Spree::Admin::ReportsController.class_eval do table_items = @line_items @include_blank = 'All' - header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item (#{currency_symbol})", "Dist (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", + header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item (#{@currency_symbol})", "Dist (#{@currency_symbol})", "Ship (#{@currency_symbol})", "Total (#{@currency_symbol})", "Paid?", "Shipping", "Delivery?", "Ship street", "Ship street 2", "Ship city", "Ship postcode", "Ship state", "Order notes"] rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address } @@ -598,6 +597,10 @@ Spree::Admin::ReportsController.class_eval do end private + + def set_currency_symbol + @currency_symbol = Spree::Money.currency_symbol + end def load_data # Load distributors either owned by the user or selling their enterprises products. diff --git a/app/models/spree/money_decorator.rb b/app/models/spree/money_decorator.rb new file mode 100644 index 0000000000..e179343bc5 --- /dev/null +++ b/app/models/spree/money_decorator.rb @@ -0,0 +1,7 @@ +Spree::Money.class_eval do + + # return the currency symbol (on it's own) for the current default currency + def self.currency_symbol + Money.new(0, Spree::Config[:currency]).symbol + end +end From c4961d250235f9998b97480d3cfa750c1320483e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 11:59:22 +1100 Subject: [PATCH 276/681] Whitespace crusader --- app/views/enterprise_mailer/welcome.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index 4f2f2e6fc8..b0a512adc1 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -32,7 +32,7 @@ Email %td   %td - %a{:href => "mailto: #{ @enterprise.email }", :target => "_blank"} + %a{:href => "mailto:#{ @enterprise.email }", :target => "_blank"} = @enterprise.email %p   From f6b08632799f4d29d53d910ed630cef7f8ccaba1 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 13:07:40 +1100 Subject: [PATCH 277/681] Make cost breakdowns consistent throughout checkout --- app/assets/stylesheets/mail/email.css.sass | 4 ++++ app/helpers/checkout_helper.rb | 19 ++++++++++--------- app/models/spree/line_item_decorator.rb | 12 +++++++++++- app/views/checkout/_summary.html.haml | 10 +++++----- .../confirm_email_for_customer.html.haml | 13 ++++++++++--- .../confirm_email_for_shop.html.haml | 18 +++++++++++++----- .../spree/shared/_order_details.html.haml | 10 +++++----- 7 files changed, 58 insertions(+), 28 deletions(-) diff --git a/app/assets/stylesheets/mail/email.css.sass b/app/assets/stylesheets/mail/email.css.sass index 3e20fb32c4..bc425a1e38 100644 --- a/app/assets/stylesheets/mail/email.css.sass +++ b/app/assets/stylesheets/mail/email.css.sass @@ -53,6 +53,10 @@ table.social background-color: white!important border: 1px solid #ebebeb +table.order-summary + border-collapse: separate + border-spacing: 0px 10px + .social .soc-btn padding: 3px 7px font-size: 12px diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index 1e672a0adc..b6c43bc412 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -6,23 +6,24 @@ module CheckoutHelper # Remove empty tax adjustments and (optionally) shipping fees adjustments.reject! { |a| a.originator_type == 'Spree::TaxRate' && a.amount == 0 } adjustments.reject! { |a| a.originator_type == 'Spree::ShippingMethod' } if exclude.include? :shipping + adjustments.reject! { |a| a.source_type == 'Spree::LineItem' } if exclude.include? :line_item enterprise_fee_adjustments = adjustments.select { |a| a.originator_type == 'EnterpriseFee' } adjustments.reject! { |a| a.originator_type == 'EnterpriseFee' } - unless exclude.include? :distribution - adjustments << Spree::Adjustment.new(label: 'Distribution', amount: enterprise_fee_adjustments.sum(&:amount)) + unless exclude.include? :admin_and_handling + adjustments << Spree::Adjustment.new(label: 'Admin & Handling', amount: enterprise_fee_adjustments.sum(&:amount)) end adjustments end - def checkout_adjustments_total(order) - adjustments = checkout_adjustments_for_summary(order, exclude: [:shipping]) - adjustments.sum &:display_amount + def checkout_line_item_adjustments(order) + adjustments = order.adjustments.eligible.where( source_type: "Spree::LineItem") + Spree::Money.new( adjustments.sum(&:amount) , { :currency => order.currency }) end - def checkout_cart_total_with_adjustments(order) - order.display_item_total.money.to_f + checkout_adjustments_total(order).money.to_f + def checkout_subtotal(order) + order.display_item_total.money.to_f + checkout_line_item_adjustments(order).money.to_f end def checkout_state_options(source_address) @@ -47,9 +48,9 @@ module CheckoutHelper name: path, id: path, "ng-model" => path, - "ng-class" => "{error: !fieldValid('#{path}')}" + "ng-class" => "{error: !fieldValid('#{path}')}" }.merge args - + render "shared/validated_input", name: name, path: path, attributes: attributes end diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index 9ce49eb67e..3333ed8735 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -23,10 +23,20 @@ Spree::LineItem.class_eval do where('spree_products.supplier_id IN (?)', enterprises) } + def price_with_adjustments + # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, + # so line_item.adjustments returns an empty array + price + order.adjustments.where(source_id: id).sum(&:amount) / quantity + end + + def single_display_amount_with_adjustments + Spree::Money.new(price_with_adjustments, { :currency => currency }) + end + def amount_with_adjustments # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, # so line_item.adjustments returns an empty array - amount + Spree::Adjustment.where(source_id: id).sum(&:amount) + amount + order.adjustments.where(source_id: id).sum(&:amount) end def display_amount_with_adjustments diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index df454e1701..d07a097db6 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -5,11 +5,11 @@ %table %tr %th Cart total - %td.cart-total.text-right= spree_number_to_currency(checkout_cart_total_with_adjustments(current_order)) + %td.cart-total.text-right= spree_number_to_currency checkout_subtotal(@order) - - checkout_adjustments_for_summary(current_order, exclude: [:shipping, :distribution]).each do |adjustment| + - checkout_adjustments_for_summary(current_order, exclude: [:shipping, :line_item]).reject{ |a| a.amount == 0 }.each do |adjustment| %tr - %th= adjustment.label + %th= adjustment.label %td.text-right= adjustment.display_amount.to_html %tr @@ -17,7 +17,7 @@ %td.shipping.text-right {{ Checkout.shippingPrice() | localizeCurrency }} %tr - %th Total + %th Total %td.total.text-right {{ Checkout.cartTotal() | localizeCurrency }} - if current_order.price_adjustment_totals.present? - current_order.price_adjustment_totals.each do |label, total| @@ -26,7 +26,7 @@ %td= total //= f.submit "Purchase", class: "button", "ofn-focus" => "accordion['payment']" - %a.button.secondary{href: cart_url} + %a.button.secondary{href: cart_url} %i.ofn-i_008-caret-left Back to Cart diff --git a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml index 65b7a202d8..9c0b57c3b2 100644 --- a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml @@ -47,11 +47,11 @@ %td{:align => "right", :colspan => "2"} Subtotal: %td{:align => "right"} - = spree_number_to_currency checkout_cart_total_with_adjustments(@order) - - checkout_adjustments_for_summary(@order, exclude: [:distribution]).reject{ |a| a.amount == 0 }.each do |adjustment| + = spree_number_to_currency checkout_subtotal(@order) + - checkout_adjustments_for_summary(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| %tr %td{:align => "right", :colspan => "2"} - = raw(adjustment.label) + = "#{raw(adjustment.label)}:" %td{:align => "right"} = adjustment.display_amount %tr @@ -83,14 +83,17 @@ - if @order.shipping_method.andand.description #{@order.shipping_method.description.html_safe} %br + %br - if @order.order_cycle.andand.pickup_time_for(@order.distributor) Delivery on: #{@order.order_cycle.pickup_time_for(@order.distributor)} %br + %br - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) Other delivery information: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} %br + %br - else @@ -102,18 +105,22 @@ - if @order.shipping_method.andand.description = @order.shipping_method.description.html_safe %br + %br - if @order.order_cycle.andand.pickup_time_for(@order.distributor) Ready for collection: #{@order.order_cycle.pickup_time_for(@order.distributor)} %br + %br - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) Collection instructions: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} %br + %br - if @order.special_instructions.present? Notes: #{@order.special_instructions} %br + %br -# Your order will be ready for collection on -# %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} diff --git a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml index 0de5b177d8..9c0b57c3b2 100644 --- a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml @@ -10,7 +10,7 @@ %p Thanks for shopping on %strong= "#{Spree::Config.site_name}." - Here are your order details from + Here are the details for you order from %strong= "#{@order.distributor.name}." %table.column{:align => "left"} %tr @@ -39,18 +39,19 @@ %td{:align => "right"} = item.quantity %td{:align => "right"} - = item.display_amount + = item.display_amount_with_adjustments + %tr %td{:colspan => "3"}   %tr %td{:align => "right", :colspan => "2"} Subtotal: %td{:align => "right"} - = spree_number_to_currency checkout_cart_total_with_adjustments(@order) - - checkout_adjustments_for_summary(@order, exclude: [:distribution]).reject{ |a| a.amount == 0 }.each do |adjustment| + = spree_number_to_currency checkout_subtotal(@order) + - checkout_adjustments_for_summary(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| %tr %td{:align => "right", :colspan => "2"} - = raw(adjustment.label) + = "#{raw(adjustment.label)}:" %td{:align => "right"} = adjustment.display_amount %tr @@ -82,14 +83,17 @@ - if @order.shipping_method.andand.description #{@order.shipping_method.description.html_safe} %br + %br - if @order.order_cycle.andand.pickup_time_for(@order.distributor) Delivery on: #{@order.order_cycle.pickup_time_for(@order.distributor)} %br + %br - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) Other delivery information: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} %br + %br - else @@ -101,18 +105,22 @@ - if @order.shipping_method.andand.description = @order.shipping_method.description.html_safe %br + %br - if @order.order_cycle.andand.pickup_time_for(@order.distributor) Ready for collection: #{@order.order_cycle.pickup_time_for(@order.distributor)} %br + %br - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) Collection instructions: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} %br + %br - if @order.special_instructions.present? Notes: #{@order.special_instructions} %br + %br -# Your order will be ready for collection on -# %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} diff --git a/app/views/spree/shared/_order_details.html.haml b/app/views/spree/shared/_order_details.html.haml index b6ff00925d..7e72432f6f 100644 --- a/app/views/spree/shared/_order_details.html.haml +++ b/app/views/spree/shared/_order_details.html.haml @@ -53,10 +53,10 @@ = truncated_product_description(item.variant.product) = "(" + item.variant.options_text + ")" unless item.variant.option_values.empty? %td.price{"data-hook" => "order_item_price"} - %span= item.single_money.to_html + %span= item.single_display_amount_with_adjustments.to_html %td{"data-hook" => "order_item_qty"}= item.quantity %td.total{"data-hook" => "order_item_total"} - %span= item.display_amount.to_html + %span= item.display_amount_with_adjustments.to_html %tfoot#order-total{"data-hook" => "order_details_total"} %tr.total @@ -82,12 +82,12 @@ %tr#subtotal-row.total %td{colspan: "4"} %b - Produce: + Produce: %td.total - %span= @order.display_item_total.to_html + %span= checkout_subtotal(@order) %tfoot#order-charges{"data-hook" => "order_details_adjustments"} - - checkout_adjustments_for_summary(@order).reverse_each do |adjustment| + - checkout_adjustments_for_summary(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| %tr.total %td{:colspan => "4"} %strong= adjustment.label + ":" From 17a2e37edaac3ede0d98aac6bf8a4f74be277101 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 13:16:29 +1100 Subject: [PATCH 278/681] Fixing broken category lookup on BPE --- .../javascripts/admin/directives/taxon_autocomplete.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee index 5f17a0d1dd..6830bd7d22 100644 --- a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee @@ -7,7 +7,7 @@ angular.module("ofn.admin").directive "ofnTaxonAutocomplete", (Taxons) -> placeholder: "Category" multiple: false initSelection: (element, callback) -> - callback Taxons.findByID(scope.product.category) + callback Taxons.findByID(scope.product.category_id) query: (query) -> query.callback { results: Taxons.findByTerm(query.term) } formatResult: (taxon) -> From f5acf367718d960645c1b09b307e80174255f136 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 14:50:19 +1100 Subject: [PATCH 279/681] Hiding enterprise shop url in ent welcome email, as that may bot actually make sense --- app/views/enterprise_mailer/welcome.html.haml | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index b0a512adc1..74840fbefa 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -10,30 +10,31 @@ Please find below all the details for viewing and editing your enterprise on %strong= "#{ Spree::Config.site_name }." We suggest keeping this email and information somewhere safe. Logging in with the account details below will allow complete access to your products and services. -%p   -%p.callout - %strong - Your enterprise details -%table{:width => "100%"} - %tr - %td{:align => "right"} - %strong - Shop URL - %td   - %td - %a{:href => "#{ main_app.shop_enterprise_url(@enterprise) }", :target => "_blank"} - = main_app.shop_enterprise_url(@enterprise) - %tr - %td   - %tr - %td{:align => "right"} - %strong - Email - %td   - %td - %a{:href => "mailto:#{ @enterprise.email }", :target => "_blank"} - = @enterprise.email +-#%p   + +-# %p.callout +-# %strong +-# Your enterprise details +-# %table{:width => "100%"} +-# %tr +-# %td{:align => "right"} +-# %strong +-# Shop URL +-# %td   +-# %td +-# %a{:href => "#{ main_app.shop_enterprise_url(@enterprise) }", :target => "_blank"} +-# = main_app.shop_enterprise_url(@enterprise) +-# %tr +-# %td   +-# %tr +-# %td{:align => "right"} +-# %strong +-# Email +-# %td   +-# %td +-# %a{:href => "mailto:#{ @enterprise.email }", :target => "_blank"} +-# = @enterprise.email %p   %p From a8dde3bbb9d0b027759c914ea87da96ae9dddb59 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 15:16:53 +1100 Subject: [PATCH 280/681] Moving shopfront trial progress bar logic into enterprises helper and dealing with expiry --- app/helpers/enterprises_helper.rb | 16 ++++++++++++++++ app/models/enterprise.rb | 10 ---------- .../add_trial_progress_bar.html.haml.deface | 3 +-- .../admin/shared/_trial_progress_bar.html.haml | 9 +++++++-- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index 1421594e97..6109842f84 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -45,4 +45,20 @@ module EnterprisesHelper options[:data] = { :action => 'remove', :confirm => enterprise_confirm_delete_message(enterprise) } link_to_with_icon 'icon-trash', name, url, options end + + def shop_trial_in_progress?(enterprise) + !!enterprise.shop_trial_start_date && + (enterprise.shop_trial_start_date + Enterprise::SHOP_TRIAL_LENGTH.days > Time.now) && + %w(own any).include?(enterprise.sells) + end + + def shop_trial_expired?(enterprise) + !!enterprise.shop_trial_start_date && + (enterprise.shop_trial_start_date + Enterprise::SHOP_TRIAL_LENGTH.days <= Time.now) && + %w(own any).include?(enterprise.sells) + end + + def remaining_trial_days(enterprise) + distance_of_time_in_words(Time.now, enterprise.shop_trial_start_date + Enterprise::SHOP_TRIAL_LENGTH.days) + end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 8d4f688565..5082267a01 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -285,16 +285,6 @@ class Enterprise < ActiveRecord::Base shipping_methods.any? && payment_methods.available.any? end - def shop_trial_in_progress? - !!shop_trial_start_date && - (shop_trial_start_date + SHOP_TRIAL_LENGTH.days > Time.now) && - %w(own any).include?(sells) - end - - def remaining_trial_days - distance_of_time_in_words(Time.now, shop_trial_start_date + SHOP_TRIAL_LENGTH.days) - end - protected def devise_mailer diff --git a/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface b/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface index bcd787cf28..eb9b44ad10 100644 --- a/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface +++ b/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface @@ -2,5 +2,4 @@ - enterprise = spree_current_user.enterprises.first if OpenFoodNetwork::Permissions.new(spree_current_user).manages_one_enterprise? -- if enterprise && enterprise.shop_trial_in_progress? - = render 'spree/admin/shared/trial_progress_bar', enterprise: enterprise \ No newline at end of file += render 'spree/admin/shared/trial_progress_bar', enterprise: enterprise \ No newline at end of file diff --git a/app/views/spree/admin/shared/_trial_progress_bar.html.haml b/app/views/spree/admin/shared/_trial_progress_bar.html.haml index 183c63a901..abffdbd6ad 100644 --- a/app/views/spree/admin/shared/_trial_progress_bar.html.haml +++ b/app/views/spree/admin/shared/_trial_progress_bar.html.haml @@ -1,2 +1,7 @@ -#trial_progress_bar - = "Your shopfront trial expires in #{enterprise.remaining_trial_days}." \ No newline at end of file +- if enterprise + -if shop_trial_in_progress?(enterprise) + #trial_progress_bar + = "Your shopfront trial expires in #{remaining_trial_days(enterprise)}." + -elsif shop_trial_expired?(enterprise) + #trial_progress_bar + = "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)." \ No newline at end of file From 8f27c643f14526c85c45c213edf779da91a09010 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 16:08:53 +1100 Subject: [PATCH 281/681] Move override for cart line item into existing view replacement --- app/overrides/cart_item_description.rb | 5 ----- app/views/spree/orders/_cart_item_description.html.haml | 7 ------- app/views/spree/orders/_line_item.html.haml | 7 +++---- 3 files changed, 3 insertions(+), 16 deletions(-) delete mode 100644 app/overrides/cart_item_description.rb delete mode 100644 app/views/spree/orders/_cart_item_description.html.haml diff --git a/app/overrides/cart_item_description.rb b/app/overrides/cart_item_description.rb deleted file mode 100644 index 8ae809a183..0000000000 --- a/app/overrides/cart_item_description.rb +++ /dev/null @@ -1,5 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/orders/_line_item", - :replace => "[data-hook='cart_item_description']", - :partial => "spree/orders/cart_item_description", - :name => "cart_item_description", - :original => 'ce2b7ddab2a6a13b25159ea18f6ab50991409d3e') diff --git a/app/views/spree/orders/_cart_item_description.html.haml b/app/views/spree/orders/_cart_item_description.html.haml deleted file mode 100644 index 8c99781d5c..0000000000 --- a/app/views/spree/orders/_cart_item_description.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%td{'data-hook' => "cart_item_description"} - %h4= variant.product.name - = variant.options_text - - if @order.insufficient_stock_lines.include? line_item - %span.out-of-stock - = variant.in_stock? ? t(:insufficient_stock, :on_hand => variant.on_hand) : t(:out_of_stock) - %br/ diff --git a/app/views/spree/orders/_line_item.html.haml b/app/views/spree/orders/_line_item.html.haml index 42649c8bee..59db1c6878 100644 --- a/app/views/spree/orders/_line_item.html.haml +++ b/app/views/spree/orders/_line_item.html.haml @@ -5,14 +5,13 @@ - else = link_to image_tag(variant.images.first.attachment.url(:small)), variant.product - %td.cart-item-description{"data-hook" => "cart_item_description"} - %h4= link_to variant.product.name, product_path(variant.product) + %td.cart-item-description{'data-hook' => "cart_item_description"} + %h4= variant.product.name = variant.options_text - if @order.insufficient_stock_lines.include? line_item %span.out-of-stock = variant.in_stock? ? t(:insufficient_stock, :on_hand => variant.on_hand) : t(:out_of_stock) - %br - = line_item_description(variant) + %br/ %td.cart-item-price{"data-hook" => "cart_item_price"} = line_item.single_money.to_html From 8a3bffb37d69293ffc02967f80bf3d54de142b7c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 18:53:29 +1100 Subject: [PATCH 282/681] Cart page conforms to convention in rest of checkout, adjusting helpers through checkout --- .../javascripts/darkswarm/cart.js.coffee | 3 +-- app/helpers/checkout_helper.rb | 20 +++++++++++++------ app/helpers/spree/orders_helper.rb | 6 ------ app/views/checkout/_summary.html.haml | 4 ++-- .../confirm_email_for_customer.html.haml | 4 ++-- .../confirm_email_for_shop.html.haml | 4 ++-- app/views/spree/orders/_adjustments.html.haml | 4 ++-- app/views/spree/orders/_form.html.haml | 8 ++++---- app/views/spree/orders/_line_item.html.haml | 4 ++-- .../spree/shared/_order_details.html.haml | 4 ++-- 10 files changed, 31 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/darkswarm/cart.js.coffee b/app/assets/javascripts/darkswarm/cart.js.coffee index bb68d0d7ec..cd12c67c4a 100644 --- a/app/assets/javascripts/darkswarm/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/cart.js.coffee @@ -13,8 +13,7 @@ $ -> $(document).ready -> $('#cart_adjustments').hide() - $('th.cart-adjustment-header').html('Distribution...') $('th.cart-adjustment-header a').click -> $('#cart_adjustments').toggle() - $('th.cart-adjustment-header a').html('Distribution') + $(this).html('Item Handling Fees (included in item totals)') false diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index b6c43bc412..cd986eb565 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -1,5 +1,5 @@ module CheckoutHelper - def checkout_adjustments_for_summary(order, opts={}) + def checkout_adjustments_for(order, opts={}) adjustments = order.adjustments.eligible exclude = opts[:exclude] || {} @@ -8,8 +8,8 @@ module CheckoutHelper adjustments.reject! { |a| a.originator_type == 'Spree::ShippingMethod' } if exclude.include? :shipping adjustments.reject! { |a| a.source_type == 'Spree::LineItem' } if exclude.include? :line_item - enterprise_fee_adjustments = adjustments.select { |a| a.originator_type == 'EnterpriseFee' } - adjustments.reject! { |a| a.originator_type == 'EnterpriseFee' } + enterprise_fee_adjustments = adjustments.select { |a| a.originator_type == 'EnterpriseFee' && a.source_type != 'Spree::LineItem' } + adjustments.reject! { |a| a.originator_type == 'EnterpriseFee' && a.source_type != 'Spree::LineItem' } unless exclude.include? :admin_and_handling adjustments << Spree::Adjustment.new(label: 'Admin & Handling', amount: enterprise_fee_adjustments.sum(&:amount)) end @@ -17,13 +17,21 @@ module CheckoutHelper adjustments end + def display_checkout_admin_and_handling_adjustments_total_for(order) + adjustments = order.adjustments.eligible.where('originator_type = ? AND source_type != ? ', 'EnterpriseFee', 'Spree::LineItem' ) + Spree::Money.new( adjustments.sum( &:amount ) , { :currency => order.currency }) + end + def checkout_line_item_adjustments(order) - adjustments = order.adjustments.eligible.where( source_type: "Spree::LineItem") - Spree::Money.new( adjustments.sum(&:amount) , { :currency => order.currency }) + order.adjustments.eligible.where( source_type: "Spree::LineItem") end def checkout_subtotal(order) - order.display_item_total.money.to_f + checkout_line_item_adjustments(order).money.to_f + order.item_total + checkout_line_item_adjustments(order).sum( &:amount ) + end + + def display_checkout_subtotal(order) + Spree::Money.new( checkout_subtotal(order) , { :currency => order.currency }) end def checkout_state_options(source_address) diff --git a/app/helpers/spree/orders_helper.rb b/app/helpers/spree/orders_helper.rb index 93b59c8e4b..5fd0f74fa8 100644 --- a/app/helpers/spree/orders_helper.rb +++ b/app/helpers/spree/orders_helper.rb @@ -5,12 +5,6 @@ module Spree order.nil? || order.line_items.empty? end - def order_distribution_subtotal(order, options={}) - options.reverse_merge! :format_as_currency => true - amount = order.adjustments.enterprise_fee.sum &:amount - options.delete(:format_as_currency) ? spree_number_to_currency(amount) : amount - end - def alternative_available_distributors(order) DistributionChangeValidator.new(order).available_distributors(Enterprise.all) - [order.distributor] end diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index d07a097db6..5bdd13187b 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -5,9 +5,9 @@ %table %tr %th Cart total - %td.cart-total.text-right= spree_number_to_currency checkout_subtotal(@order) + %td.cart-total.text-right= display_checkout_subtotal(@order) - - checkout_adjustments_for_summary(current_order, exclude: [:shipping, :line_item]).reject{ |a| a.amount == 0 }.each do |adjustment| + - checkout_adjustments_for(current_order, exclude: [:shipping, :line_item]).reject{ |a| a.amount == 0 }.each do |adjustment| %tr %th= adjustment.label %td.text-right= adjustment.display_amount.to_html diff --git a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml index 9c0b57c3b2..49f23087ed 100644 --- a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml @@ -47,8 +47,8 @@ %td{:align => "right", :colspan => "2"} Subtotal: %td{:align => "right"} - = spree_number_to_currency checkout_subtotal(@order) - - checkout_adjustments_for_summary(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + = display_checkout_subtotal(@order) + - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| %tr %td{:align => "right", :colspan => "2"} = "#{raw(adjustment.label)}:" diff --git a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml index 9c0b57c3b2..49f23087ed 100644 --- a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml @@ -47,8 +47,8 @@ %td{:align => "right", :colspan => "2"} Subtotal: %td{:align => "right"} - = spree_number_to_currency checkout_subtotal(@order) - - checkout_adjustments_for_summary(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + = display_checkout_subtotal(@order) + - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| %tr %td{:align => "right", :colspan => "2"} = "#{raw(adjustment.label)}:" diff --git a/app/views/spree/orders/_adjustments.html.haml b/app/views/spree/orders/_adjustments.html.haml index 21e971832a..71579e3ace 100644 --- a/app/views/spree/orders/_adjustments.html.haml +++ b/app/views/spree/orders/_adjustments.html.haml @@ -1,10 +1,10 @@ %thead %tr{"data-hook" => "cart_adjustments_headers"} %th.cart-adjustment-header{colspan: "6"} - Distribution Fees + %a{ href: "#" } Fees... %tbody#cart_adjustments{"data-hook" => ""} - - @order.adjustments.eligible.each do |adjustment| + - checkout_line_item_adjustments(@order).each do |adjustment| %tr %td{colspan: "4"}= adjustment.label %td= adjustment.display_amount.to_html diff --git a/app/views/spree/orders/_form.html.haml b/app/views/spree/orders/_form.html.haml index 953ea83e18..435ff03ff6 100644 --- a/app/views/spree/orders/_form.html.haml +++ b/app/views/spree/orders/_form.html.haml @@ -22,13 +22,13 @@ %tfoot#edit-cart %tr %td - Product + Produce Subtotal \: - %span.order-total.item-total= spree_number_to_currency(@order.item_total) + %span.order-total.item-total= display_checkout_subtotal(@order) %td - Distribution + Admin & Handling \: - %span.order-total.distribution-total= order_distribution_subtotal(@order) + %span.order-total.distribution-total= display_checkout_admin_and_handling_adjustments_total_for(@order) %td %td = button_tag :class => 'secondary radius expand small', :id => 'update-button' do diff --git a/app/views/spree/orders/_line_item.html.haml b/app/views/spree/orders/_line_item.html.haml index 59db1c6878..027e16978a 100644 --- a/app/views/spree/orders/_line_item.html.haml +++ b/app/views/spree/orders/_line_item.html.haml @@ -14,11 +14,11 @@ %br/ %td.cart-item-price{"data-hook" => "cart_item_price"} - = line_item.single_money.to_html + = line_item.single_display_amount_with_adjustments.to_html %td.cart-item-quantity{"data-hook" => "cart_item_quantity"} = item_form.number_field :quantity, :min => 0, :class => "line_item_quantity", :size => 5 %td.cart-item-total{"data-hook" => "cart_item_total"} - = line_item.display_amount.to_html unless line_item.quantity.nil? + = line_item.display_amount_with_adjustments.to_html unless line_item.quantity.nil? %td.cart-item-delete.text-center{"data-hook" => "cart_item_delete"} {{ quantity }} diff --git a/app/views/spree/shared/_order_details.html.haml b/app/views/spree/shared/_order_details.html.haml index 7e72432f6f..a3308ced34 100644 --- a/app/views/spree/shared/_order_details.html.haml +++ b/app/views/spree/shared/_order_details.html.haml @@ -84,10 +84,10 @@ %b Produce: %td.total - %span= checkout_subtotal(@order) + %span= display_checkout_subtotal(@order) %tfoot#order-charges{"data-hook" => "order_details_adjustments"} - - checkout_adjustments_for_summary(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| %tr.total %td{:colspan => "4"} %strong= adjustment.label + ":" From 0cf2df8358005c7bd1255d25c5f7e8975f208fba Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 18:54:50 +1100 Subject: [PATCH 283/681] Remove obsolete text version of order confirm_email --- .../order_mailer/confirm_email.text.haml | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 app/views/spree/order_mailer/confirm_email.text.haml diff --git a/app/views/spree/order_mailer/confirm_email.text.haml b/app/views/spree/order_mailer/confirm_email.text.haml deleted file mode 100644 index c9ad50fb63..0000000000 --- a/app/views/spree/order_mailer/confirm_email.text.haml +++ /dev/null @@ -1,63 +0,0 @@ -Dear #{@order.bill_address.firstname}, - -Please review and retain the following order information for your records. -\ -============================================================ -Order Summary -============================================================ -Order for: #{@order.bill_address.full_name} -- @order.line_items.each do |item| - #{item.variant.sku} #{raw(item.variant.product.supplier.name)} #{raw(item.variant.product.name)} #{raw(item.variant.options_text)} (QTY: #{item.quantity}) @ #{item.single_money} = #{item.display_amount} -============================================================ -Subtotal: #{number_to_currency checkout_cart_total_with_adjustments(@order)} -- checkout_adjustments_for_summary(@order, exclude: [:distribution]).each do |adjustment| - #{raw(adjustment.label)} #{adjustment.display_amount} -Order Total: #{@order.display_total} -- if @order.payments.first.andand.payment_method.andand.type == "Spree::PaymentMethod::Check" and @order.payments.first.andand.payment_method.andand.description - \ - ============================================================ - Payment Details - ============================================================ - #{@order.payments.first.andand.payment_method.andand.description.andand.html_safe} - -- if @order.shipping_method.andand.require_ship_address - \ - ============================================================ - Delivery Details - ============================================================ - Your order will be delivered to: - #{@order.ship_address.to_s} - - - if @order.shipping_method.andand.description - #{@order.shipping_method.description.html_safe} - - - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - Delivery on: #{@order.order_cycle.pickup_time_for(@order.distributor)} - - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) - Other delivery information: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - -- else - \ - ============================================================ - Collection Details - ============================================================ - - if @order.shipping_method.andand.description - #{@order.shipping_method.description.html_safe} - - - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - Ready for collection: #{@order.order_cycle.pickup_time_for(@order.distributor)} - - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) - Collection instructions: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - - - if @order.special_instructions.present? - notes: #{@order.special_instructions} -\ -Thanks for your support. - -#{@order.distributor.contact}, -= @order.distributor.name -= @order.distributor.phone -= @order.distributor.email -= @order.distributor.website From 258573f5a67b328366e041e7d0f4e1846d2efd05 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 18:58:26 +1100 Subject: [PATCH 284/681] Remove obsolete text version of user signup_confirmation --- .../spree/user_mailer/signup_confirmation.text.haml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 app/views/spree/user_mailer/signup_confirmation.text.haml diff --git a/app/views/spree/user_mailer/signup_confirmation.text.haml b/app/views/spree/user_mailer/signup_confirmation.text.haml deleted file mode 100644 index 218e90e8bc..0000000000 --- a/app/views/spree/user_mailer/signup_confirmation.text.haml +++ /dev/null @@ -1,12 +0,0 @@ -Hello, - -Welcome to #{ENV['DEFAULT_COUNTRY']}'s Open Food Network! Your login email is #{@user.email} - -You can go online and start shopping through food hubs and local producers you like at http://openfoodnetwork.org.au - -We welcome all your questions and feedback; you can use the Send Feedback button on the site or email us at hello@openfoodnetwork.org - -Thanks for getting on board and we look forward to introducing you to many more great farmers, food hubs and food! - -Cheers, -Kirsten Larsen and the OFN Team From 38440dd70a43414f0d253f5a45090ef3659b1340 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 26 Nov 2014 23:16:11 +1100 Subject: [PATCH 285/681] Ammending checkout specs for two emails on placement of order --- spec/features/consumer/shopping/checkout_spec.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 667e7abdf8..d46f0ae2f1 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -80,6 +80,7 @@ feature "As a consumer I want to check out my cart", js: true do context "on the checkout page with payments open" do before do + ActionMailer::Base.deliveries.clear visit checkout_path checkout_as_guest toggle_payment @@ -118,6 +119,8 @@ feature "As a consumer I want to check out my cart", js: true do choose pm1.name end + + ActionMailer::Base.deliveries.length.should == 0 place_order page.should have_content "Your order has been processed successfully" ActionMailer::Base.deliveries.length.should == 2 @@ -211,6 +214,7 @@ feature "As a consumer I want to check out my cart", js: true do context "when the customer has a pre-set shipping and billing address" do before do + ActionMailer::Base.deliveries.clear # Load up the customer's order and give them a shipping and billing address # This is equivalent to when the customer has ordered before and their addresses # are pre-populated. @@ -227,9 +231,10 @@ feature "As a consumer I want to check out my cart", js: true do toggle_payment choose pm1.name + expect(ActionMailer::Base.deliveries.length).to be 0 place_order page.should have_content "Your order has been processed successfully" - ActionMailer::Base.deliveries.length.should == 2 + expect(ActionMailer::Base.deliveries.length).to be 2 end end end From 45bcee16efc6ed560e2e10038b7ff1d755b21e5b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 27 Nov 2014 09:44:13 +1100 Subject: [PATCH 286/681] Pulling out obsolete setup in BOM spec to make it more like BPE spec, see if that helps with intermittent fails --- spec/features/admin/bulk_order_management_spec.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index fe18f1a9bf..faba967e47 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -5,20 +5,16 @@ feature %q{ I want to be able to manage orders in bulk } , js: true do include AuthenticationWorkflow - include AuthorizationHelpers include WebHelper - after { Warden.test_reset! } - stub_authorization! context "listing orders" do before :each do - admin_user = quick_login_as_admin + login_to_admin_section end it "displays a message when number of line items is zero" do visit '/admin/orders/bulk_management' page.should have_text "No orders found." - end context "displaying the list of line items" do From 9782a9d3d6011811cdbaced61ee5ddef80f7c20a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 27 Nov 2014 14:42:02 +1100 Subject: [PATCH 287/681] Hopefully fixing Poltergeist Timeout error due to long long time for first admin spec --- spec/features/admin/authentication_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/features/admin/authentication_spec.rb b/spec/features/admin/authentication_spec.rb index c4768e5eed..beb8c31c2b 100644 --- a/spec/features/admin/authentication_spec.rb +++ b/spec/features/admin/authentication_spec.rb @@ -5,7 +5,11 @@ feature "Authentication", js: true do let(:user) { create(:user, password: "password", password_confirmation: "password") } scenario "logging into admin redirects home, then back to admin" do - visit spree.admin_path + # This is the first admin spec, so give a little extra load time for slow systems + Capybara.using_wait_time(60) do + visit spree.admin_path + end + fill_in "Email", with: user.email fill_in "Password", with: user.password click_login_button From ef0b0a9a94921dcb81fa13a7d91cd7d048b2cef0 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 27 Nov 2014 12:59:25 +1100 Subject: [PATCH 288/681] Replacing fixtures with minimal seeding Seeding: 1 country: Australia 2 states: Vic, NSW --- spec/spec_helper.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f421715a82..167d90b72a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,9 +28,17 @@ require 'spree/api/testing_support/helpers' require 'spree/api/testing_support/helpers_decorator' require 'spree/core/testing_support/authorization_helpers' -require 'active_record/fixtures' -fixtures_dir = File.expand_path('../../db/default', __FILE__) -ActiveRecord::Fixtures.create_fixtures(fixtures_dir, ['spree/states', 'spree/countries']) +if Spree::Country.first.nil? + Spree::Country.create!({"name"=>"Australia", "iso3"=>"AUS", "iso"=>"AU", "iso_name"=>"AUSTRALIA", "numcode"=>"36"}, :without_protection => true) + country = Spree::Country.find_by_name('Australia') + Spree::State.create!({"name"=>"Victoria", "abbr"=>"Vic", :country=>country}, :without_protection => true) + Spree::State.create!({"name"=>"New South Wales", "abbr"=>"NSW", :country=>country}, :without_protection => true) +end + +# TODO: remove duplicate code with config/initializers/spree.rb +Spree.config do |config| + config.default_country_id = Spree::Country.find_by_name('Australia').id +end # Capybara config require 'capybara/poltergeist' From 699c9a62a5b35da92d486fc5151a0fc044ecd772 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 27 Nov 2014 16:39:14 +1100 Subject: [PATCH 289/681] Removing pretty_inspect so that bugsnag alert work on production --- app/models/spree/order_decorator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index c1cfb463b3..2e60d2bcec 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -98,10 +98,10 @@ Spree::Order.class_eval do current_item = find_line_item_by_variant(variant) if current_item Bugsnag.notify(RuntimeError.new("Order populator weirdness"), { - current_item: current_item.as_json.pretty_inspect, + current_item: current_item.as_json, line_items: line_items.map(&:id), reloaded: line_items(:reload).map(&:id), - variant: variant.as_json.pretty_inspect + variant: variant.as_json }) current_item.quantity = quantity current_item.max_quantity = max_quantity From 35c27bf516f91c13025f0818f98e5db8836567a3 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Thu, 27 Nov 2014 20:36:41 +0000 Subject: [PATCH 290/681] First specs for additional scope to order model. Not liking the repeated code so would appreciate feedback to get rid of it. Thanks! --- spec/models/spree/order_spec.rb | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 63c315a18a..9326368e87 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -77,7 +77,7 @@ describe Spree::Order do subject.update_distribution_charge! end - it "ensures the correct adjustment(s) are created for order cycles" do + it "ensures the correct adjustment(s) are created for order cycles" do EnterpriseFee.stub(:clear_all_adjustments_on_order) line_item = double(:line_item) subject.stub(:line_items) { [line_item] } @@ -320,15 +320,39 @@ describe Spree::Order do end end - describe "scopes" do + describe "scopes" do describe "not_state" do it "finds only orders not in specified state" do o = FactoryGirl.create(:completed_order_with_totals) o.cancel! - Spree::Order.not_state(:canceled).should_not include o end end + + describe "with payment method name" do + + it "returns the order with payment method name" do + my_payment_method = FactoryGirl.create(:payment_method, :name => "PM Test") + my_order = FactoryGirl.create(:order) + payment1 = FactoryGirl.create(:payment, :order => my_order, :payment_method => my_payment_method) + Spree::Order.with_payment_method_name("PM Test").should include my_order + end + + it "doesn't return rows with a different payment method name" do + my_payment_method = FactoryGirl.create(:payment_method, :name => "PM Test") + my_order = FactoryGirl.create(:order) + payment1 = FactoryGirl.create(:payment, :order => my_order, :payment_method => my_payment_method) + Spree::Order.with_payment_method_name("PM Test2").should_not include my_order + end + + it "doesn't return duplicate rows" do + my_payment_method = FactoryGirl.create(:payment_method, :name => "PM Test") + my_order = FactoryGirl.create(:order) + payment1 = FactoryGirl.create(:payment, :order => my_order, :payment_method => my_payment_method) + payment2 = FactoryGirl.create(:payment, :order => my_order, :payment_method => my_payment_method) + Spree::Order.with_payment_method_name("PM Test").length.should == 1 + end + end end describe "shipping address prepopulation" do @@ -365,5 +389,4 @@ describe Spree::Order do order.create_shipment! end end - end From 082a3cd9abb86e1bf049f7775f7cbd081d13e8c1 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 28 Nov 2014 12:29:47 +1100 Subject: [PATCH 291/681] Creating simple_order_cycle instead of order_cycle Speedup on my machine: 1 minute 44.52 seconds 21.9 seconds --- spec/controllers/checkout_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/checkout_controller_spec.rb b/spec/controllers/checkout_controller_spec.rb index dcbd1d5177..0499febfc9 100644 --- a/spec/controllers/checkout_controller_spec.rb +++ b/spec/controllers/checkout_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe CheckoutController do let(:distributor) { double(:distributor) } - let(:order_cycle) { create(:order_cycle) } + let(:order_cycle) { create(:simple_order_cycle) } let(:order) { create(:order) } before do order.stub(:checkout_allowed?).and_return true From 7f764db4d72e1a2e91e795b0a47087e9a12e484b Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 28 Nov 2014 12:39:05 +1100 Subject: [PATCH 292/681] Using simple_order_cycle order_cycle: 1 minute 56.88 seconds simple_order_cycle: 1 minute 8.05 seconds --- spec/features/consumer/home_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/consumer/home_spec.rb b/spec/features/consumer/home_spec.rb index d08dee5dfc..f5b8091432 100644 --- a/spec/features/consumer/home_spec.rb +++ b/spec/features/consumer/home_spec.rb @@ -8,7 +8,7 @@ feature 'Home', js: true do let!(:invisible_distributor) { create(:distributor_enterprise, visible: false) } let(:d1) { create(:distributor_enterprise) } let(:d2) { create(:distributor_enterprise) } - let!(:order_cycle) { create(:order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } + let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } let!(:producer) { create(:supplier_enterprise) } let!(:er) { create(:enterprise_relationship, parent: distributor, child: producer) } From ccd03bfa8479ff4c831a3e7e6e13d5d53c583fa5 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 28 Nov 2014 13:20:05 +1100 Subject: [PATCH 293/681] Using simple_order_cycle Test timings were not accurate. Output: 4 order_cycles: 9.94 seconds 2 order_cycles: 9.87 seconds 0 order_cycles: 9.9 seconds Felt execution was much higher --- spec/models/spree/product_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index c6acae0850..f9a990830c 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -359,8 +359,8 @@ module Spree d2 = create(:distributor_enterprise) p1 = create(:product) p2 = create(:product) - oc1 = create(:order_cycle, :distributors => [d1], :variants => [p1.master]) - oc2 = create(:order_cycle, :distributors => [d2], :variants => [p2.master]) + oc1 = create(:simple_order_cycle, :distributors => [d1], :variants => [p1.master]) + oc2 = create(:simple_order_cycle, :distributors => [d2], :variants => [p2.master]) p1.should be_in_distributor d1 p1.should_not be_in_distributor d2 @@ -371,8 +371,8 @@ module Spree d2 = create(:distributor_enterprise) p1 = create(:product) p2 = create(:product) - oc1 = create(:order_cycle, :distributors => [d1], :variants => [p1.master]) - oc2 = create(:order_cycle, :distributors => [d2], :variants => [p2.master]) + oc1 = create(:simple_order_cycle, :distributors => [d1], :variants => [p1.master]) + oc2 = create(:simple_order_cycle, :distributors => [d2], :variants => [p2.master]) p1.should be_in_order_cycle oc1 p1.should_not be_in_order_cycle oc2 From aa2cefb88cebe366af8de37f7a981d1166443730 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 28 Nov 2014 13:45:25 +1100 Subject: [PATCH 294/681] Using simple_order_cycle where applicable Before: 3 minutes 0 seconds After: 1 minute 21.02 seconds --- spec/models/order_cycle_spec.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index 56aa51a0d0..f436690850 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -91,25 +91,25 @@ describe OrderCycle do end it "finds the most recently closed order cycles" do - oc1 = create(:order_cycle, orders_close_at: 2.hours.ago) - oc2 = create(:order_cycle, orders_close_at: 1.hour.ago) - oc3 = create(:order_cycle, orders_close_at: 1.hour.from_now) + oc1 = create(:simple_order_cycle, orders_close_at: 2.hours.ago) + oc2 = create(:simple_order_cycle, orders_close_at: 1.hour.ago) + oc3 = create(:simple_order_cycle, orders_close_at: 1.hour.from_now) OrderCycle.most_recently_closed.should == [oc2, oc1] end it "finds the soonest opening order cycles" do - oc1 = create(:order_cycle, orders_open_at: 1.weeks.from_now) - oc2 = create(:order_cycle, orders_open_at: 2.hours.from_now) - oc3 = create(:order_cycle, orders_open_at: 1.hour.ago) + oc1 = create(:simple_order_cycle, orders_open_at: 1.weeks.from_now) + oc2 = create(:simple_order_cycle, orders_open_at: 2.hours.from_now) + oc3 = create(:simple_order_cycle, orders_open_at: 1.hour.ago) OrderCycle.soonest_opening.should == [oc2, oc1] end it "finds the soonest closing order cycles" do - oc1 = create(:order_cycle, orders_close_at: 2.hours.ago) - oc2 = create(:order_cycle, orders_close_at: 2.hour.from_now) - oc3 = create(:order_cycle, orders_close_at: 1.hour.from_now) + oc1 = create(:simple_order_cycle, orders_close_at: 2.hours.ago) + oc2 = create(:simple_order_cycle, orders_close_at: 2.hour.from_now) + oc3 = create(:simple_order_cycle, orders_close_at: 1.hour.from_now) OrderCycle.soonest_closing.should == [oc3, oc2] end @@ -311,7 +311,7 @@ describe OrderCycle do end describe "checking status" do - let(:oc) { create(:order_cycle) } + let(:oc) { create(:simple_order_cycle) } it "reports status when an order cycle is upcoming" do Timecop.freeze(oc.orders_open_at - 1.second) do From 73b8f37d77266214ff96c5edbd6057049212883e Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 28 Nov 2014 13:55:06 +1100 Subject: [PATCH 295/681] Using simple_order_cycle Before: 2 minutes 8.7 seconds After: 16.11 seconds --- spec/controllers/api/order_cycles_controller_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/controllers/api/order_cycles_controller_spec.rb b/spec/controllers/api/order_cycles_controller_spec.rb index 7f8bbec7af..a9cceac796 100644 --- a/spec/controllers/api/order_cycles_controller_spec.rb +++ b/spec/controllers/api/order_cycles_controller_spec.rb @@ -6,8 +6,8 @@ module Api include Spree::Api::TestingSupport::Helpers render_views - let!(:oc1) { FactoryGirl.create(:order_cycle) } - let!(:oc2) { FactoryGirl.create(:order_cycle) } + let!(:oc1) { FactoryGirl.create(:simple_order_cycle) } + let!(:oc2) { FactoryGirl.create(:simple_order_cycle) } let(:coordinator) { oc1.coordinator } let(:attributes) { [:id, :name, :suppliers, :distributors] } @@ -70,7 +70,7 @@ module Api user.save! user end - let!(:order_cycle) { create(:order_cycle, suppliers: [oc_supplier], distributors: [oc_distributor]) } + let!(:order_cycle) { create(:simple_order_cycle, suppliers: [oc_supplier], distributors: [oc_distributor]) } context "as the user of a supplier to an order cycle" do before :each do From 349b7de11a875382e4c76507a6a35b2d3fac9a85 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 28 Nov 2014 14:02:55 +1100 Subject: [PATCH 296/681] Using simple_order_cycle Before: 2 minutes 58.3 seconds After: 35.04 seconds --- spec/controllers/shop_controller_spec.rb | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index f1c132bb24..bfa68da912 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -16,21 +16,21 @@ describe ShopController do describe "Selecting order cycles" do it "should select an order cycle when only one order cycle is open" do - oc1 = create(:order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [d]) spree_get :show controller.current_order_cycle.should == oc1 end it "should not set an order cycle when multiple order cycles are open" do - oc1 = create(:order_cycle, distributors: [d]) - oc2 = create(:order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [d]) + oc2 = create(:simple_order_cycle, distributors: [d]) spree_get :show controller.current_order_cycle.should == nil end it "should allow the user to post to select the current order cycle" do - oc1 = create(:order_cycle, distributors: [d]) - oc2 = create(:order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [d]) + oc2 = create(:simple_order_cycle, distributors: [d]) spree_post :order_cycle, order_cycle_id: oc2.id response.should be_success @@ -40,8 +40,8 @@ describe ShopController do context "RABL tests" do render_views it "should return the order cycle details when the oc is selected" do - oc1 = create(:order_cycle, distributors: [d]) - oc2 = create(:order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [d]) + oc2 = create(:simple_order_cycle, distributors: [d]) spree_post :order_cycle, order_cycle_id: oc2.id response.should be_success @@ -49,7 +49,7 @@ describe ShopController do end it "should return the current order cycle when hit with GET" do - oc1 = create(:order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [d]) controller.stub(:current_order_cycle).and_return oc1 spree_get :order_cycle response.body.should have_content oc1.id @@ -57,9 +57,9 @@ describe ShopController do 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]) - oc3 = create(:order_cycle, distributors: [create(:distributor_enterprise)]) + oc1 = create(:simple_order_cycle, distributors: [d]) + oc2 = create(:simple_order_cycle, distributors: [d]) + oc3 = create(:simple_order_cycle, distributors: [create(:distributor_enterprise)]) spree_post :order_cycle, order_cycle_id: oc3.id response.status.should == 404 @@ -71,7 +71,7 @@ describe ShopController do describe "producers/suppliers" do let(:supplier) { create(:supplier_enterprise) } let(:product) { create(:product, supplier: supplier) } - let(:order_cycle) { create(:order_cycle, distributors: [d], coordinator: create(:distributor_enterprise)) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator: create(:distributor_enterprise)) } before do exchange = Exchange.find(order_cycle.exchanges.to_enterprises(d).outgoing.first.id) @@ -81,7 +81,7 @@ describe ShopController do describe "returning products" do let(:product) { create(:product) } - let(:order_cycle) { create(:order_cycle, distributors: [d], coordinator: create(:distributor_enterprise)) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator: create(:distributor_enterprise)) } let(:exchange) { Exchange.find(order_cycle.exchanges.to_enterprises(d).outgoing.first.id) } before do From e240933b29ac0f1b9b12ed13049bfb47943ad1ef Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 28 Nov 2014 14:45:35 +1100 Subject: [PATCH 297/681] Using simple_order_cycle in spec Before: 4 minutes 6.9 seconds After: 3 minutes 27.4 seconds --- spec/features/admin/order_cycles_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 3d310df1ba..180485a734 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -360,9 +360,9 @@ feature %q{ scenario "updating many order cycle opening/closing times at once" do # Given three order cycles - oc1 = create(:order_cycle) - oc2 = create(:order_cycle) - oc3 = create(:order_cycle) + oc1 = create(:simple_order_cycle) + oc2 = create(:simple_order_cycle) + oc3 = create(:simple_order_cycle) # When I go to the order cycles page login_to_admin_section @@ -394,7 +394,7 @@ feature %q{ scenario "cloning an order cycle" do # Given an order cycle - oc = create(:order_cycle) + oc = create(:simple_order_cycle) # When I clone it login_to_admin_section @@ -625,7 +625,7 @@ feature %q{ end it "shows me an index of order cycles without enterprise columns" do - create(:order_cycle, coordinator: enterprise) + create(:simple_order_cycle, coordinator: enterprise) visit admin_order_cycles_path page.should_not have_selector 'th', text: 'SUPPLIERS' page.should_not have_selector 'th', text: 'COORDINATOR' From 8e280919ac7fd5d9b240e68b8a1506592eb3022d Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 28 Nov 2014 15:36:34 +1100 Subject: [PATCH 298/681] Using simple_order_cycle in clone spec Creating a coordinator fee and two exchanges in the spec instead of using a full order_cycle. Timing of this single test: Before: 15.32 seconds After: 6.26 seconds --- spec/models/order_cycle_spec.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index f436690850..7ba2b66589 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -349,23 +349,30 @@ describe OrderCycle do end it "clones itself" do - oc = create(:order_cycle) - occ = oc.clone! + coordinator = create(:enterprise); + oc = create(:simple_order_cycle, coordinator_fees: [create(:enterprise_fee, enterprise: coordinator)]) + ex1 = create(:exchange, order_cycle: oc) + ex2 = create(:exchange, order_cycle: oc) + oc.clone! occ = OrderCycle.last occ.name.should == "COPY OF #{oc.name}" occ.orders_open_at.should be_nil occ.orders_close_at.should be_nil + occ.coordinator.should_not be_nil occ.coordinator.should == oc.coordinator + occ.coordinator_fee_ids.should_not be_empty occ.coordinator_fee_ids.should == oc.coordinator_fee_ids - #(0..occ.exchanges.count).all? { |i| occ.exchanges[i].eql? oc.exchanges[i] }.should be_true - # to_h gives us a unique hash for each exchange + # check that the clone has no additional exchanges occ.exchanges.map(&:to_h).all? do |ex| oc.exchanges.map(&:to_h).include? ex end + # check that the clone has original exchanges + occ.exchanges.map(&:to_h).include? ex1.to_h + occ.exchanges.map(&:to_h).include? ex2.to_h end describe "finding recently closed order cycles" do From c2c51a55316b0974cb4b9ac71eb3433795707045 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Tue, 2 Dec 2014 12:24:34 +0000 Subject: [PATCH 299/681] Fixing typo in feature spec --- spec/features/admin/reports_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 82cdbf59ba..d53c072a4b 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -65,7 +65,7 @@ feature %q{ scenario "order payment method report" do click_link "Order Cycle Management" - rows = find("table#listing_order_payment_method").all("thead tr") + rows = find("table#listing_order_payment_methods").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } table.sort.should == [ ["First Name", "Last Name", "Email", "Phone", "Hub", "Payment Method", "Amount", "Amount Paid"] From a5ae1c490cadd7f62bb1a155fd1fe69b380b814f Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Tue, 2 Dec 2014 17:48:04 +0000 Subject: [PATCH 300/681] Fixing typo in report found through testing --- lib/open_food_network/order_cycle_management_report.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index 2087f901de..0f722e94d3 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -40,7 +40,7 @@ module OpenFoodNetwork end def filter_active (orders) - orders.complete.where("spree_orders.state != ?", :cancelled) + orders.complete.where("spree_orders.state != ?", :canceled) end def filter_to_payment_method (orders) From 73ece4075c36df2a52b77f6b290f5172e2a20cc5 Mon Sep 17 00:00:00 2001 From: Paul Mackay Date: Thu, 20 Nov 2014 05:59:04 +0000 Subject: [PATCH 301/681] Add rails-i18n gem. --- Gemfile | 1 + Gemfile.lock | 4 ++++ app/mailers/spree/user_mailer_decorator.rb | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 7843846852..3475cdfe68 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,7 @@ source 'https://rubygems.org' ruby "1.9.3" gem 'rails', '3.2.19' +gem 'rails-i18n', '~> 3.0.0' gem 'pg' gem 'spree', :github => 'openfoodfoundation/spree', :branch => '1-3-stable' diff --git a/Gemfile.lock b/Gemfile.lock index e2b771255f..aa79d5f674 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -397,6 +397,9 @@ GEM activesupport (= 3.2.19) bundler (~> 1.0) railties (= 3.2.19) + rails-i18n (3.0.1) + i18n (~> 0.5) + rails (>= 3.0.0, < 4.0.0) railties (3.2.19) actionpack (= 3.2.19) activesupport (= 3.2.19) @@ -568,6 +571,7 @@ DEPENDENCIES rack-livereload rack-ssl rails (= 3.2.19) + rails-i18n (~> 3.0.0) representative_view roadie-rails (~> 1.0.3) rspec-rails diff --git a/app/mailers/spree/user_mailer_decorator.rb b/app/mailers/spree/user_mailer_decorator.rb index 40313d1b66..98e396d4e8 100644 --- a/app/mailers/spree/user_mailer_decorator.rb +++ b/app/mailers/spree/user_mailer_decorator.rb @@ -2,6 +2,6 @@ Spree::UserMailer.class_eval do def signup_confirmation(user) @user = user mail(:to => user.email, :from => from_address, - :subject => 'Welcome to ' + Spree::Config[:site_name]) + :subject => t(:welcome_to) + Spree::Config[:site_name]) end end From 26bca0faf84a753d70d304e16d6a5a266364d2f4 Mon Sep 17 00:00:00 2001 From: Paul Mackay Date: Tue, 25 Nov 2014 09:33:00 +0000 Subject: [PATCH 302/681] Localize more strings on home page. --- app/views/home/_producer-register.html.haml | 5 +++-- app/views/shared/_footer.html.haml | 13 +++++++------ config/locales/en-GB.yml | 2 ++ config/locales/en.yml | 2 ++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/views/home/_producer-register.html.haml b/app/views/home/_producer-register.html.haml index dd92974705..62845bd9ea 100644 --- a/app/views/home/_producer-register.html.haml +++ b/app/views/home/_producer-register.html.haml @@ -2,9 +2,10 @@ .row .small-12.columns.text-center - %h2 Aussie Producers + %h2 + = t :producers %h5 Want to join the Open Food Network? - %br + %br %a.neutral-btn.turquoise{href: "/register"} Register now %i.ofn-i_007-caret-right diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index 3791bd130d..a4c47239ac 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -30,28 +30,29 @@ %a{href: "/groups"} Groups .small-12.medium-2.columns.text-left %h4 Producers - %p Australian producers are now welcome to join the Open Food Network. + %p + = t :footer_producers %p %a{href: "/register"} Register now .small-12.medium-3.columns.text-left %h4 About us - %p OFN is a network of independent online food stores that connect farmers and food hubs with individuals and local businesses. It gives farmers and food hubs an easier and fairer way to distribute their food. + %p OFN is a network of independent online food stores that connect farmers and food hubs with individuals and local businesses. It gives farmers and food hubs an easier and fairer way to distribute their food. .row.landing-page-row - .small-12.columns.text-center.pad-top + .small-12.columns.text-center.pad-top %hr %h5.pad-top %a{title: 'Open Food Network', href:'http://www.openfoodnetwork.org', target: '_blank' } openfoodnetwork.org %br © Copyright 2014 Open Food Foundation %p - %small + %small %a{href:"https://creativecommons.org/licenses/by-sa/3.0/", target: "_blank" } Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) %p - %small + %small %a{href:"/Terms-of-service.pdf", target: "_blank" } Site terms & conditions | %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank" } Open Source & developer info on GitHub // To be added when Guy's pretty landing page is up: - //| + //| //%a{href:'' } Developers diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 71186a0a52..becbebd80e 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -19,3 +19,5 @@ en-GB: welcome_to: 'Welcome to ' search_by_name: Search by name... + producers: UK Producers + footer_producers: UK producers are now welcome to join Open Food Network UK. diff --git a/config/locales/en.yml b/config/locales/en.yml index e1aadad6b8..3ce90292ba 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -15,3 +15,5 @@ en: confirmation_not_sent: "Could not send a confirmation email." home: "OFN" search_by_name: Search by name or suburb... + producers: Aussie Producers + footer_producers: Australian producers are now welcome to join the Open Food Network. From 3358fb3d7311b7d08a1b7c8ba9627885b1e91c8f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 26 Nov 2014 14:45:33 +1100 Subject: [PATCH 303/681] Name based on meaning instead of context --- app/views/shared/_footer.html.haml | 2 +- config/locales/en-GB.yml | 2 +- config/locales/en.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index a4c47239ac..70eb14dd41 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -31,7 +31,7 @@ .small-12.medium-2.columns.text-left %h4 Producers %p - = t :footer_producers + = t :producers_join %p %a{href: "/register"} Register now .small-12.medium-3.columns.text-left diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index becbebd80e..41a02f2de2 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -20,4 +20,4 @@ en-GB: welcome_to: 'Welcome to ' search_by_name: Search by name... producers: UK Producers - footer_producers: UK producers are now welcome to join Open Food Network UK. + producers_join: UK producers are now welcome to join Open Food Network UK. diff --git a/config/locales/en.yml b/config/locales/en.yml index 3ce90292ba..b44bf79506 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -16,4 +16,4 @@ en: home: "OFN" search_by_name: Search by name or suburb... producers: Aussie Producers - footer_producers: Australian producers are now welcome to join the Open Food Network. + producers_join: Australian producers are now welcome to join the Open Food Network. From 633a8a49e2d88e2d6b8f38e4f876ebd0aee4261d Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Wed, 3 Dec 2014 00:35:53 +0000 Subject: [PATCH 304/681] updating spec based on the wise advice of Rohan --- spec/models/spree/order_spec.rb | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 9326368e87..f06402cc02 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -330,27 +330,25 @@ describe Spree::Order do end describe "with payment method name" do - + let!(:o1) { create(:order) } + let!(:o2) { create(:order) } + let!(:pm1) { create(:payment_method, name: 'foo') } + let!(:pm2) { create(:payment_method, name: 'bar') } + let!(:p1) { create(:payment, order: o1, payment_method: pm1) } + let!(:p2) { create(:payment, order: o2, payment_method: pm2) } + it "returns the order with payment method name" do - my_payment_method = FactoryGirl.create(:payment_method, :name => "PM Test") - my_order = FactoryGirl.create(:order) - payment1 = FactoryGirl.create(:payment, :order => my_order, :payment_method => my_payment_method) - Spree::Order.with_payment_method_name("PM Test").should include my_order + Spree::Order.with_payment_method_name('foo').should == [o1] end it "doesn't return rows with a different payment method name" do - my_payment_method = FactoryGirl.create(:payment_method, :name => "PM Test") - my_order = FactoryGirl.create(:order) - payment1 = FactoryGirl.create(:payment, :order => my_order, :payment_method => my_payment_method) - Spree::Order.with_payment_method_name("PM Test2").should_not include my_order + Spree::Order.with_payment_method_name('foobar').should_not include o1 + Spree::Order.with_payment_method_name('foobar').should_not include o2 end it "doesn't return duplicate rows" do - my_payment_method = FactoryGirl.create(:payment_method, :name => "PM Test") - my_order = FactoryGirl.create(:order) - payment1 = FactoryGirl.create(:payment, :order => my_order, :payment_method => my_payment_method) - payment2 = FactoryGirl.create(:payment, :order => my_order, :payment_method => my_payment_method) - Spree::Order.with_payment_method_name("PM Test").length.should == 1 + p2 = FactoryGirl.create(:payment, :order => o1, :payment_method => pm1) + Spree::Order.with_payment_method_name('foo').length.should == 1 end end end From 50e26ed96fb62f18fc115ee6b9cb43d377401d4d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Dec 2014 12:22:43 +1100 Subject: [PATCH 305/681] Remove old required partial --- app/views/admin/enterprises/_required.html.haml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 app/views/admin/enterprises/_required.html.haml diff --git a/app/views/admin/enterprises/_required.html.haml b/app/views/admin/enterprises/_required.html.haml deleted file mode 100644 index 1b3574de9f..0000000000 --- a/app/views/admin/enterprises/_required.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -.with-tip{'data-powertip' => 'Required - You need to fill this field to complete the form.'} - %a{style: 'color: red; font-size: 110%'} * From 95f8334370115fb8a7f0e0aa9e6c69c8480201c1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Dec 2014 12:28:22 +1100 Subject: [PATCH 306/681] Reinstate style --- app/assets/stylesheets/admin/openfoodnetwork.css.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 8bf19ea6b0..6df859fd09 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -231,3 +231,8 @@ text-angular { } } } + +span.required { + color: red; + font-size: 110%; +} From 19667f31fa1ed06ceeda55399eb906bcfa737b66 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 3 Dec 2014 15:19:25 +1100 Subject: [PATCH 307/681] Add missing translation for welcome_to --- config/locales/en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index b44bf79506..fe5bafa19f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -14,6 +14,7 @@ en: confirmation_sent: "Confirmation email sent!" confirmation_not_sent: "Could not send a confirmation email." home: "OFN" + welcome_to: 'Welcome to ' search_by_name: Search by name or suburb... producers: Aussie Producers producers_join: Australian producers are now welcome to join the Open Food Network. From 5c191bcc7fdc775ebc53bee0ede916bc7a706d08 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 4 Dec 2014 15:38:59 +1100 Subject: [PATCH 308/681] Moving test seeding into spec/support/seeds.rb --- spec/spec_helper.rb | 12 ------------ spec/support/seeds.rb | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 spec/support/seeds.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 167d90b72a..3a7d2b3ca1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,18 +28,6 @@ require 'spree/api/testing_support/helpers' require 'spree/api/testing_support/helpers_decorator' require 'spree/core/testing_support/authorization_helpers' -if Spree::Country.first.nil? - Spree::Country.create!({"name"=>"Australia", "iso3"=>"AUS", "iso"=>"AU", "iso_name"=>"AUSTRALIA", "numcode"=>"36"}, :without_protection => true) - country = Spree::Country.find_by_name('Australia') - Spree::State.create!({"name"=>"Victoria", "abbr"=>"Vic", :country=>country}, :without_protection => true) - Spree::State.create!({"name"=>"New South Wales", "abbr"=>"NSW", :country=>country}, :without_protection => true) -end - -# TODO: remove duplicate code with config/initializers/spree.rb -Spree.config do |config| - config.default_country_id = Spree::Country.find_by_name('Australia').id -end - # Capybara config require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist diff --git a/spec/support/seeds.rb b/spec/support/seeds.rb new file mode 100644 index 0000000000..ed08b26561 --- /dev/null +++ b/spec/support/seeds.rb @@ -0,0 +1,18 @@ +# Minimal test seeding +# -------------------- +# +# Countries and states are seeded once in the beginning. The database cleaner +# leaves them there when deleting the rest (see spec/spec_helper.rb). +# You can add more entries here if you need them for your tests. + +if Spree::Country.scoped.empty? + Spree::Country.create!({"name"=>"Australia", "iso3"=>"AUS", "iso"=>"AU", "iso_name"=>"AUSTRALIA", "numcode"=>"36"}, :without_protection => true) + country = Spree::Country.find_by_name('Australia') + Spree::State.create!({"name"=>"Victoria", "abbr"=>"Vic", :country=>country}, :without_protection => true) + Spree::State.create!({"name"=>"New South Wales", "abbr"=>"NSW", :country=>country}, :without_protection => true) +end + +# Since the country seeding differs from other environments, the default +# country id has to be updated here. This line can be removed as soon as the +# default country id is replaced by something database independent. +Spree::Config.default_country_id = Spree::Country.find_by_name('Australia').id From 7b41d4c5d4ef4698c05df1eabb814f6500f64064 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 5 Dec 2014 14:56:32 +1100 Subject: [PATCH 309/681] Denoting the product category as required Using common style: span.required * --- app/views/spree/admin/products/_primary_taxon_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/products/_primary_taxon_form.html.haml b/app/views/spree/admin/products/_primary_taxon_form.html.haml index 23b2a0c8f3..8ffe5887a7 100644 --- a/app/views/spree/admin/products/_primary_taxon_form.html.haml +++ b/app/views/spree/admin/products/_primary_taxon_form.html.haml @@ -1,6 +1,6 @@ = f.field_container :primary_taxon_id do = f.label :primary_taxon_id, t(:product_category) - * + %span.required * %br = f.collection_select(:primary_taxon_id, Spree::Taxon.all, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"}) = f.error_message_on :primary_taxon_id From 3cc79d0036e63de8c163a42f7f1a2f7a91cc0c24 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 5 Dec 2014 15:52:10 +1100 Subject: [PATCH 310/681] Filter deleted products in products report BugHerd 484: Remove deleted products from Product / Inventory reports --- lib/open_food_network/products_and_inventory_report.rb | 6 +++++- .../open_food_network/products_and_inventory_report_spec.rb | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb index 487ccfa67d..0e6eb086f1 100644 --- a/lib/open_food_network/products_and_inventory_report.rb +++ b/lib/open_food_network/products_and_inventory_report.rb @@ -60,7 +60,11 @@ module OpenFoodNetwork def filter(variants) # NOTE: Ordering matters. # filter_to_order_cycle and filter_to_distributor return Arrays not Arel - filter_to_distributor filter_to_order_cycle filter_on_hand filter_to_supplier variants + filter_to_distributor filter_to_order_cycle filter_on_hand filter_to_supplier filter_not_deleted variants + end + + def filter_not_deleted(variants) + variants.where("spree_variants.deleted_at is null") end def filter_on_hand(variants) diff --git a/spec/lib/open_food_network/products_and_inventory_report_spec.rb b/spec/lib/open_food_network/products_and_inventory_report_spec.rb index 9e449219e6..2e23919fe6 100644 --- a/spec/lib/open_food_network/products_and_inventory_report_spec.rb +++ b/spec/lib/open_food_network/products_and_inventory_report_spec.rb @@ -115,6 +115,12 @@ module OpenFoodNetwork product2 = create(:simple_product, supplier: supplier) subject.filter(Spree::Variant.scoped).sort.should == [product1.master, product2.master].sort end + it "should filter deleted products" do + product1 = create(:simple_product, supplier: supplier) + product2 = create(:simple_product, supplier: supplier) + product2.delete + subject.filter(Spree::Variant.scoped).sort.should == [product1.master].sort + end describe "based on report type" do it "returns only variants on hand" do product1 = create(:simple_product, supplier: supplier, on_hand: 99) From f878e1803724d1d98ab38a0f5e32998179c9a4b6 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Mon, 8 Dec 2014 16:25:18 +0000 Subject: [PATCH 311/681] Update that works better with the specs --- .../order_cycle_management_report.rb | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index 0f722e94d3..1db4804c33 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -28,23 +28,15 @@ module OpenFoodNetwork end def orders - filter Spree::Order + filter Spree::Order.managed_by(@user).distributed_by_user(@user).complete.where("spree_orders.state != ?", :canceled) end def filter(orders) - filter_for_user filter_active filter_to_order_cycle filter_to_payment_method filter_to_distribution orders - end - - def filter_for_user (orders) - orders.managed_by(@user).distributed_by_user(@user) - end - - def filter_active (orders) - orders.complete.where("spree_orders.state != ?", :canceled) + filter_to_order_cycle filter_to_payment_method filter_to_distribution orders end def filter_to_payment_method (orders) - if params[:payment_method_name].present? + if params[:payment_method_name].to_i > 0 orders.with_payment_method_name(params[:payment_method_name]) else orders @@ -52,7 +44,7 @@ module OpenFoodNetwork end def filter_to_distribution (orders) - if params[:distribution_name].present? + if params[:distribution_name].to_i > 0 orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:distribution_name]) else orders From 2f83d71931ea4dbbdb33653fcb04a9ed1335e730 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 10 Dec 2014 09:56:33 +1100 Subject: [PATCH 312/681] Adding mail/all.css to assets precompile list --- config/application.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/application.rb b/config/application.rb index d3bbdbf3fd..885902e8f4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -92,6 +92,7 @@ module Openfoodnetwork config.assets.precompile += ['store/all.css', 'store/all.js', 'store/shop_front.js', 'iehack.js'] config.assets.precompile += ['admin/all.css', 'admin/restore_spree_from_cms.css', 'admin/*.js', 'admin/**/*.js'] config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all.js'] + config.assets.precompile += ['mail/all.css'] config.assets.precompile += ['comfortable_mexican_sofa/*'] config.assets.precompile += ['search/all.css', 'search/*.js'] config.assets.precompile += ['shared/*'] From 27896534f0aeb79d3948475757ae7781ba100c1d Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 11 Dec 2014 12:05:59 +1100 Subject: [PATCH 313/681] Navigation Callbacks (confirmation to leave) refs A navigation callback could give only a fixed value at load time. Now it can act depending on the site's state just before the page is unloaded. --- .../admin/utils/directives/navigation_check.js.coffee | 5 +++-- app/views/admin/enterprises/_ng_form.html.haml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee index 95f52505eb..da25286505 100644 --- a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee @@ -4,6 +4,7 @@ angular.module("admin.utils").directive "navCheck", (NavigationCheck)-> navCallback: '&' link: (scope,element,attributes) -> # Define navigationCallback on a controller in scope, otherwise this default will be used: - scope.navCallback ||= -> + callback = scope.navCallback() + callback ||= -> "You will lose any unsaved work!" - NavigationCheck.register(scope.navCallback) + NavigationCheck.register(callback) diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 86be5bb0be..d3ea41d88f 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -1,4 +1,4 @@ -= form_for [main_app, :admin, @enterprise], html: { name: "enterprise", "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl' , "nav-check" => '', "nav-callback" => 'enterpriseNavCallback()' } do |f| += form_for [main_app, :admin, @enterprise], html: { name: "enterprise", "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl' , "nav-check" => '', "nav-callback" => 'enterpriseNavCallback' } do |f| .row .sixteen.columns.alpha .eleven.columns.alpha.fullwidth_inputs From 11f1261dd3feddb98569660a196fd77298bc6bf2 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 11 Dec 2014 12:07:48 +1100 Subject: [PATCH 314/681] Navigation Callbacks for WebKit --- .../admin/utils/services/navigation_check.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee index ff1041474c..8bdab1bc9c 100644 --- a/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee +++ b/app/assets/javascripts/admin/utils/services/navigation_check.js.coffee @@ -15,7 +15,8 @@ angular.module("admin.utils") onBeforeUnloadHandler: ($event) => message = @getMessage() if message - ($event or $window.event).preventDefault() + # following: https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload + ($event or $window.event).returnValue = message message # Action for angular navigation. From 76739a47460c22cd203ecbce17b27561402a9c36 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 11 Dec 2014 12:14:51 +1100 Subject: [PATCH 315/681] Unify nav-check and nav-callback attributes in nav-check-callback The value of nav-check was not used and nav-callback was only important if nav-check was given. So we need only one attribute, now named nav-check-callback. --- .../admin/utils/directives/navigation_check.js.coffee | 8 ++++---- app/views/admin/enterprises/_ng_form.html.haml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee index da25286505..fee80e9acf 100644 --- a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee @@ -1,10 +1,10 @@ -angular.module("admin.utils").directive "navCheck", (NavigationCheck)-> +angular.module("admin.utils").directive "navCheckCallback", (NavigationCheck)-> restrict: 'A' scope: - navCallback: '&' + navCheckCallback: '&' link: (scope,element,attributes) -> - # Define navigationCallback on a controller in scope, otherwise this default will be used: - callback = scope.navCallback() + # Provide a callback, otherwise this default will be used: + callback = scope.navCheckCallback() callback ||= -> "You will lose any unsaved work!" NavigationCheck.register(callback) diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index d3ea41d88f..feb46b840f 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -1,4 +1,4 @@ -= form_for [main_app, :admin, @enterprise], html: { name: "enterprise", "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl' , "nav-check" => '', "nav-callback" => 'enterpriseNavCallback' } do |f| += form_for [main_app, :admin, @enterprise], html: { name: "enterprise", "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl', "nav-check-callback" => 'enterpriseNavCallback' } do |f| .row .sixteen.columns.alpha .eleven.columns.alpha.fullwidth_inputs From 440044372b48259b4c584759888c5e022642e531 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 11 Dec 2014 14:27:08 +1100 Subject: [PATCH 316/681] Leave-page-warning only after changing inputs The enterprise form is now set 'dirty' whenever an input changes. The navigation callback confirms to leave the page only if the form is dirty. --- .../controllers/enterprise_controller.js.coffee | 3 ++- app/views/admin/enterprises/_ng_form.html.haml | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index e9a1bc077f..4c14a3b096 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -12,7 +12,8 @@ angular.module("admin.enterprises") # from a directive "nav-check" in the page - if we pass it here it will be called in the test suite, # and on all new uses of this contoller, and we might not want that . $scope.enterpriseNavCallback = -> - "You are editing an enterprise!" + if $scope.enterprise.$dirty + "Your changes to the enterprise are not saved yet." for payment_method in $scope.PaymentMethods payment_method.selected = payment_method.id in $scope.Enterprise.payment_method_ids diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index feb46b840f..e0a9bef86e 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -1,4 +1,14 @@ -= form_for [main_app, :admin, @enterprise], html: { name: "enterprise", "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl', "nav-check-callback" => 'enterpriseNavCallback' } do |f| +-# Not all inputs are ng inputs, they don't make the form dirty on change. +-# ng-change is only valid for inputs, not for a form. +-# So we use onchange and have to get the scope to access the ng controller +-# The nav-check-callback is warning on leave if the form is dirty. += form_for [main_app, :admin, @enterprise], html: { name: "enterprise", + "ng-app" => 'admin.enterprises', + "ng-submit" => "navClear()", + "ng-controller" => 'enterpriseCtrl', + "nav-check-callback" => 'enterpriseNavCallback', + 'onchange' => 'angular.element(enterprise).scope().enterprise.$setDirty()', + } do |f| .row .sixteen.columns.alpha .eleven.columns.alpha.fullwidth_inputs From d0e013c1a581bf9fe95f67fff7a4fe9730dd879a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 11 Dec 2014 14:50:29 +1100 Subject: [PATCH 317/681] Removing obsolete checkout views --- .../spree/checkout/_distributor.html.haml | 4 --- .../_other_available_distributors.html.erb | 13 ------- app/views/spree/checkout/_summary.html.erb | 36 ------------------- .../spree/checkout/payment/_gateway.html.haml | 25 ------------- .../spree/checkout/payment/_paypal.html.haml | 0 5 files changed, 78 deletions(-) delete mode 100644 app/views/spree/checkout/_distributor.html.haml delete mode 100644 app/views/spree/checkout/_other_available_distributors.html.erb delete mode 100644 app/views/spree/checkout/_summary.html.erb delete mode 100644 app/views/spree/checkout/payment/_gateway.html.haml delete mode 100644 app/views/spree/checkout/payment/_paypal.html.haml diff --git a/app/views/spree/checkout/_distributor.html.haml b/app/views/spree/checkout/_distributor.html.haml deleted file mode 100644 index 24f656709b..0000000000 --- a/app/views/spree/checkout/_distributor.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -.columns.omega.six - %fieldset#shipping - %legend Distributor - = render 'enterprises/distributor_details', :distributor => @order.distributor diff --git a/app/views/spree/checkout/_other_available_distributors.html.erb b/app/views/spree/checkout/_other_available_distributors.html.erb deleted file mode 100644 index 765c71640a..0000000000 --- a/app/views/spree/checkout/_other_available_distributors.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<% if @order.state == 'address' %> -
    - <% unless alternative_available_distributors(@order).empty? %> - <%= form_for(@order) do |f| %> - <%= f.label :distributor_label, "Alternative distributors for this order:" %> - <%= f.select :distributor_id, options_for_select( enterprises_options(alternative_available_distributors(@order)) ) %> - <%= f.submit "Change Distributor" %> - <% end %> - <% else %> - No alternative distributors available. - <% end %> -
    -<% end %> diff --git a/app/views/spree/checkout/_summary.html.erb b/app/views/spree/checkout/_summary.html.erb deleted file mode 100644 index 125254177e..0000000000 --- a/app/views/spree/checkout/_summary.html.erb +++ /dev/null @@ -1,36 +0,0 @@ - -

    <%= t(:order_summary) %>

    - - - - - - - - - - <% adjustments = checkout_adjustments_for_summary(order) %> - <% adjustments.each do |adjustment| %> - - - - - <% end %> - - - - - - - <% if order.price_adjustment_totals.present? %> - - <% @order.price_adjustment_totals.each do |label, total| %> - - - - - <% end %> - - <% end %> - -
    <%= t(:item_total) %>:<%= order.display_item_total %>
    <%= adjustment.label %>: <%= adjustment.display_amount.to_html %>
    <%= t(:order_total) %>:<%= @order.display_total.to_html %>
    <%= label %><%= total %>
    diff --git a/app/views/spree/checkout/payment/_gateway.html.haml b/app/views/spree/checkout/payment/_gateway.html.haml deleted file mode 100644 index 0b801cb420..0000000000 --- a/app/views/spree/checkout/payment/_gateway.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -.row - .small-6.columns - %label - First Name - %input{type: :text, disabled: true, "ng-value" => "order.bill_address.firstname"} - - .small-6.columns - %label - Last Name - %input{type: :text, disabled: true, "ng-value" => "order.bill_address.lastname"} - - .small-6.columns - = validated_input "Card Number", "secrets.card_number", required: true, maxlength: 19, autocomplete: "off" - .small-6.columns - = validated_input "Security Code", "secrets.card_verification_value", required: true - -.row - .small-12.columns - %label{for: "secrets.card_month"} Expiry Date - -.row - .small-6.columns - %select{"ng-model" => "secrets.card_month", "ng-options" => "currMonth.value as currMonth.key for currMonth in months", name: "secrets.card_month", required: true} - .small-6.columns - %select{"ng-model" => "secrets.card_year", "ng-options" => "year for year in years", name: "secrets.card_year", required: true} diff --git a/app/views/spree/checkout/payment/_paypal.html.haml b/app/views/spree/checkout/payment/_paypal.html.haml deleted file mode 100644 index e69de29bb2..0000000000 From 3be437485aa3b67b217ffd9fd060bdd2d7d1c4f7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 11 Dec 2014 14:53:11 +1100 Subject: [PATCH 318/681] Redirecting any spree checkout_state redirects to our checkout controller --- .../spree/checkout_controller_decorator.rb | 7 ++++++- .../controllers/spree/checkout_controller_spec.rb | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/controllers/spree/checkout_controller_decorator.rb b/app/controllers/spree/checkout_controller_decorator.rb index 5f6f7826e1..061599870f 100644 --- a/app/controllers/spree/checkout_controller_decorator.rb +++ b/app/controllers/spree/checkout_controller_decorator.rb @@ -2,6 +2,11 @@ Spree::CheckoutController.class_eval do include CheckoutHelper + def edit + flash.keep + redirect_to main_app.checkout_path + end + private def before_payment @@ -18,7 +23,7 @@ Spree::CheckoutController.class_eval do preferred_bill_address, preferred_ship_address = spree_current_user.bill_address, spree_current_user.ship_address if spree_current_user.respond_to?(:bill_address) && spree_current_user.respond_to?(:ship_address) @order.bill_address ||= preferred_bill_address || last_used_bill_address || Spree::Address.default - @order.ship_address ||= preferred_ship_address || last_used_ship_address || nil + @order.ship_address ||= preferred_ship_address || last_used_ship_address || nil end def after_complete diff --git a/spec/controllers/spree/checkout_controller_spec.rb b/spec/controllers/spree/checkout_controller_spec.rb index 08817a01b9..b9b85c92fa 100644 --- a/spec/controllers/spree/checkout_controller_spec.rb +++ b/spec/controllers/spree/checkout_controller_spec.rb @@ -1,8 +1,11 @@ require 'spec_helper' require 'spree/api/testing_support/helpers' +require 'support/request/authentication_workflow' describe Spree::CheckoutController do + include AuthenticationWorkflow + context "After completing an order" do it "should create a new empty order" do controller.current_order(true) @@ -34,6 +37,18 @@ describe Spree::CheckoutController do controller.current_order.token.should == order.token session[:access_token].should == order.token end + end + context "rendering edit from within spree for the current checkout state" do + let!(:order) { controller.current_order(true) } + let!(:line_item) { create(:line_item, order: order) } + let!(:user) { create_enterprise_user } + + it "redirects to the OFN checkout page" do + controller.stub(:skip_state_validation?) { true } + controller.stub(:spree_current_user) { user } + spree_get :edit + response.should redirect_to checkout_path + end end end From eab2e5de2aada10aaba95d9dd722cdc8c7bcfc17 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Dec 2014 15:00:44 +1100 Subject: [PATCH 319/681] Use scope --- lib/open_food_network/products_and_inventory_report.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb index 0e6eb086f1..1cb485efee 100644 --- a/lib/open_food_network/products_and_inventory_report.rb +++ b/lib/open_food_network/products_and_inventory_report.rb @@ -64,7 +64,7 @@ module OpenFoodNetwork end def filter_not_deleted(variants) - variants.where("spree_variants.deleted_at is null") + variants.not_deleted end def filter_on_hand(variants) From dfb9e5bde5fdf59138d7f911e44e219fbe927c77 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 11 Dec 2014 15:48:29 +1100 Subject: [PATCH 320/681] Order reports now present the price of line items with adjustments included --- .../admin/reports_controller_decorator.rb | 24 ++++++++-------- app/models/spree/order_decorator.rb | 4 +-- spec/models/spree/order_spec.rb | 28 ++++++++++++------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 622edbe9a9..0aa41dbc05 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -393,8 +393,8 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| line_items.first.variant.full_name }, proc { |line_items| line_items.sum { |li| li.quantity } }, proc { |line_items| total_units(line_items) }, - proc { |line_items| line_items.first.variant.price }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.amount } }, proc { |line_items| "" }, proc { |line_items| "incoming transport" } ] @@ -416,8 +416,8 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| line_items.first.variant.full_name }, proc { |line_items| line_items.first.order.distributor.name }, proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.first.variant.price }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.amount } }, proc { |line_items| "shipping method" } ] rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, @@ -432,7 +432,7 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| "TOTAL" }, proc { |line_items| "" }, proc { |line_items| "" }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, + proc { |line_items| line_items.sum { |li| li.amount } }, proc { |line_items| "" } ] }, { group_by: proc { |line_item| line_item.order.distributor }, sort_by: proc { |distributor| distributor.name } } ] @@ -448,8 +448,8 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| line_items.first.variant.product.name }, proc { |line_items| line_items.first.variant.full_name }, proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.first.variant.price }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.amount } }, proc { |line_items| "" }, proc { |line_items| "shipping method" } ] @@ -461,7 +461,7 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| "" }, proc { |line_items| "" }, proc { |line_items| "" }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, + proc { |line_items| line_items.sum { |li| li.amount } }, proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, proc { |line_items| "" } ] }, { group_by: proc { |line_item| line_item.variant.product.supplier }, @@ -488,7 +488,7 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| line_items.first.variant.product.name }, proc { |line_items| line_items.first.variant.full_name }, proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, + proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, proc { |line_items| "" }, proc { |line_items| "" }, proc { |line_items| "" }, @@ -516,8 +516,8 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| "TOTAL" }, proc { |line_items| "" }, proc { |line_items| "" }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, - proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.distribution_total } }, + proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.admin_and_handling_total } }, proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.total } }, proc { |line_items| line_items.all? { |li| li.order.paid? } ? "Yes" : "No" }, @@ -547,7 +547,7 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| line_items.first.variant.product.name }, proc { |line_items| line_items.first.variant.full_name }, proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.first.variant.price }, + proc { |line_items| line_items.first.price }, proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, proc { |line_items| "" }, proc { |line_items| "incoming transport" } ] diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 2e60d2bcec..00ee7999f5 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -176,8 +176,8 @@ Spree::Order.class_eval do line_items.map(&:variant) end - def distribution_total - adjustments.eligible.where(originator_type: 'EnterpriseFee').sum(&:amount) + def admin_and_handling_total + adjustments.eligible.where("originator_type = ? AND source_type != ?", 'EnterpriseFee', 'Spree::LineItem').sum(&:amount) end # Show payment methods for this distributor diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 3322a1672e..72c00d9ec2 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -147,34 +147,42 @@ describe Spree::Order do end end - describe "getting the distribution charge" do + describe "getting the admin and handling charge" do let(:o) { create(:order) } let(:li) { create(:line_item, order: o) } it "returns the sum of eligible enterprise fee adjustments" do - ef = create(:enterprise_fee) + ef = create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new ) ef.calculator.set_preference :amount, 123.45 - a = ef.create_locked_adjustment("adjustment", li.order, li, true) + a = ef.create_locked_adjustment("adjustment", o, o, true) - o.distribution_total.should == 123.45 + o.admin_and_handling_total.should == 123.45 end it "does not include ineligible adjustments" do - ef = create(:enterprise_fee) + ef = create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new ) ef.calculator.set_preference :amount, 123.45 - a = ef.create_locked_adjustment("adjustment", li.order, li, true) + a = ef.create_locked_adjustment("adjustment", o, o, true) a.update_column :eligible, false - o.distribution_total.should == 0 + o.admin_and_handling_total.should == 0 end it "does not include adjustments that do not originate from enterprise fees" do - sm = create(:shipping_method) + sm = create(:shipping_method, calculator: Spree::Calculator::FlatRate.new ) sm.calculator.set_preference :amount, 123.45 - sm.create_adjustment("adjustment", li.order, li, true) + sm.create_adjustment("adjustment", o, o, true) - o.distribution_total.should == 0 + o.admin_and_handling_total.should == 0 + end + + it "does not include adjustments whose source is a line item" do + ef = create(:enterprise_fee, calculator: Spree::Calculator::PerItem.new ) + ef.calculator.set_preference :amount, 123.45 + ef.create_adjustment("adjustment", li.order, li, true) + + o.admin_and_handling_total.should == 0 end end From b7810df346c2e4bb9f5980c698d1ae6df78fbf4a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 11 Dec 2014 16:09:17 +1100 Subject: [PATCH 321/681] Revert "Removing obsolete checkout views" This reverts commit d0e013c1a581bf9fe95f67fff7a4fe9730dd879a. --- .../spree/checkout/_distributor.html.haml | 4 +++ .../_other_available_distributors.html.erb | 13 +++++++ app/views/spree/checkout/_summary.html.erb | 36 +++++++++++++++++++ .../spree/checkout/payment/_gateway.html.haml | 25 +++++++++++++ .../spree/checkout/payment/_paypal.html.haml | 0 5 files changed, 78 insertions(+) create mode 100644 app/views/spree/checkout/_distributor.html.haml create mode 100644 app/views/spree/checkout/_other_available_distributors.html.erb create mode 100644 app/views/spree/checkout/_summary.html.erb create mode 100644 app/views/spree/checkout/payment/_gateway.html.haml create mode 100644 app/views/spree/checkout/payment/_paypal.html.haml diff --git a/app/views/spree/checkout/_distributor.html.haml b/app/views/spree/checkout/_distributor.html.haml new file mode 100644 index 0000000000..24f656709b --- /dev/null +++ b/app/views/spree/checkout/_distributor.html.haml @@ -0,0 +1,4 @@ +.columns.omega.six + %fieldset#shipping + %legend Distributor + = render 'enterprises/distributor_details', :distributor => @order.distributor diff --git a/app/views/spree/checkout/_other_available_distributors.html.erb b/app/views/spree/checkout/_other_available_distributors.html.erb new file mode 100644 index 0000000000..765c71640a --- /dev/null +++ b/app/views/spree/checkout/_other_available_distributors.html.erb @@ -0,0 +1,13 @@ +<% if @order.state == 'address' %> +
    + <% unless alternative_available_distributors(@order).empty? %> + <%= form_for(@order) do |f| %> + <%= f.label :distributor_label, "Alternative distributors for this order:" %> + <%= f.select :distributor_id, options_for_select( enterprises_options(alternative_available_distributors(@order)) ) %> + <%= f.submit "Change Distributor" %> + <% end %> + <% else %> + No alternative distributors available. + <% end %> +
    +<% end %> diff --git a/app/views/spree/checkout/_summary.html.erb b/app/views/spree/checkout/_summary.html.erb new file mode 100644 index 0000000000..125254177e --- /dev/null +++ b/app/views/spree/checkout/_summary.html.erb @@ -0,0 +1,36 @@ + +

    <%= t(:order_summary) %>

    + + + + + + + + + + <% adjustments = checkout_adjustments_for_summary(order) %> + <% adjustments.each do |adjustment| %> + + + + + <% end %> + + + + + + + <% if order.price_adjustment_totals.present? %> + + <% @order.price_adjustment_totals.each do |label, total| %> + + + + + <% end %> + + <% end %> + +
    <%= t(:item_total) %>:<%= order.display_item_total %>
    <%= adjustment.label %>: <%= adjustment.display_amount.to_html %>
    <%= t(:order_total) %>:<%= @order.display_total.to_html %>
    <%= label %><%= total %>
    diff --git a/app/views/spree/checkout/payment/_gateway.html.haml b/app/views/spree/checkout/payment/_gateway.html.haml new file mode 100644 index 0000000000..0b801cb420 --- /dev/null +++ b/app/views/spree/checkout/payment/_gateway.html.haml @@ -0,0 +1,25 @@ +.row + .small-6.columns + %label + First Name + %input{type: :text, disabled: true, "ng-value" => "order.bill_address.firstname"} + + .small-6.columns + %label + Last Name + %input{type: :text, disabled: true, "ng-value" => "order.bill_address.lastname"} + + .small-6.columns + = validated_input "Card Number", "secrets.card_number", required: true, maxlength: 19, autocomplete: "off" + .small-6.columns + = validated_input "Security Code", "secrets.card_verification_value", required: true + +.row + .small-12.columns + %label{for: "secrets.card_month"} Expiry Date + +.row + .small-6.columns + %select{"ng-model" => "secrets.card_month", "ng-options" => "currMonth.value as currMonth.key for currMonth in months", name: "secrets.card_month", required: true} + .small-6.columns + %select{"ng-model" => "secrets.card_year", "ng-options" => "year for year in years", name: "secrets.card_year", required: true} diff --git a/app/views/spree/checkout/payment/_paypal.html.haml b/app/views/spree/checkout/payment/_paypal.html.haml new file mode 100644 index 0000000000..e69de29bb2 From b9011d9abe67229de8e7803afda34b3233ae36e1 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 11 Dec 2014 16:17:15 +1100 Subject: [PATCH 322/681] Remove obsolete spree checkout views --- .../spree/checkout/_distributor.html.haml | 4 --- .../_other_available_distributors.html.erb | 13 ------- app/views/spree/checkout/_summary.html.erb | 36 ------------------- 3 files changed, 53 deletions(-) delete mode 100644 app/views/spree/checkout/_distributor.html.haml delete mode 100644 app/views/spree/checkout/_other_available_distributors.html.erb delete mode 100644 app/views/spree/checkout/_summary.html.erb diff --git a/app/views/spree/checkout/_distributor.html.haml b/app/views/spree/checkout/_distributor.html.haml deleted file mode 100644 index 24f656709b..0000000000 --- a/app/views/spree/checkout/_distributor.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -.columns.omega.six - %fieldset#shipping - %legend Distributor - = render 'enterprises/distributor_details', :distributor => @order.distributor diff --git a/app/views/spree/checkout/_other_available_distributors.html.erb b/app/views/spree/checkout/_other_available_distributors.html.erb deleted file mode 100644 index 765c71640a..0000000000 --- a/app/views/spree/checkout/_other_available_distributors.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<% if @order.state == 'address' %> -
    - <% unless alternative_available_distributors(@order).empty? %> - <%= form_for(@order) do |f| %> - <%= f.label :distributor_label, "Alternative distributors for this order:" %> - <%= f.select :distributor_id, options_for_select( enterprises_options(alternative_available_distributors(@order)) ) %> - <%= f.submit "Change Distributor" %> - <% end %> - <% else %> - No alternative distributors available. - <% end %> -
    -<% end %> diff --git a/app/views/spree/checkout/_summary.html.erb b/app/views/spree/checkout/_summary.html.erb deleted file mode 100644 index 125254177e..0000000000 --- a/app/views/spree/checkout/_summary.html.erb +++ /dev/null @@ -1,36 +0,0 @@ - -

    <%= t(:order_summary) %>

    - - - - - - - - - - <% adjustments = checkout_adjustments_for_summary(order) %> - <% adjustments.each do |adjustment| %> - - - - - <% end %> - - - - - - - <% if order.price_adjustment_totals.present? %> - - <% @order.price_adjustment_totals.each do |label, total| %> - - - - - <% end %> - - <% end %> - -
    <%= t(:item_total) %>:<%= order.display_item_total %>
    <%= adjustment.label %>: <%= adjustment.display_amount.to_html %>
    <%= t(:order_total) %>:<%= @order.display_total.to_html %>
    <%= label %><%= total %>
    From 079781576b07ab596b3ca724156e608cf16e19f8 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Thu, 11 Dec 2014 12:47:56 +0000 Subject: [PATCH 323/681] Adding new specs and a couple updates the lib/report --- .../order_cycle_management_report.rb | 6 +- .../order_cycle_management_report_spec.rb | 127 ++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 spec/lib/open_food_network/order_cycle_management_report_spec.rb diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index 1db4804c33..0ac838c027 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -36,7 +36,7 @@ module OpenFoodNetwork end def filter_to_payment_method (orders) - if params[:payment_method_name].to_i > 0 + if params[:payment_method_name].present? orders.with_payment_method_name(params[:payment_method_name]) else orders @@ -44,7 +44,7 @@ module OpenFoodNetwork end def filter_to_distribution (orders) - if params[:distribution_name].to_i > 0 + if params[:distribution_name].present? orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:distribution_name]) else orders @@ -52,7 +52,7 @@ module OpenFoodNetwork end def filter_to_order_cycle(orders) - if params[:order_cycle_id].to_i > 0 + if params[:order_cycle_id].present? orders.where(order_cycle_id: params[:order_cycle_id]) else orders diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb new file mode 100644 index 0000000000..74069f6849 --- /dev/null +++ b/spec/lib/open_food_network/order_cycle_management_report_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +module OpenFoodNetwork + describe OrderCycleManagementReport do + context "as a site admin" do + let(:user) do + user = create(:user) + user.spree_roles << Spree::Role.find_or_create_by_name!("admin") + user + end + subject { OrderCycleManagementReport.new user } + + describe "fetching orders" do + it "fetches completed orders" do + o1 = create(:order) + o2 = create(:order, completed_at: 1.day.ago) + subject.orders.should == [o2] + end + + it "does not show cancelled orders" do + o1 = create(:order, state: "canceled", completed_at: 1.day.ago) + o2 = create(:order, completed_at: 1.day.ago) + subject.orders.should == [o2] + end + end + end + + context "as an enterprise user" do + let(:user) do + user = create(:user) + user.spree_roles = [] + user.save! + user + end + + subject { OrderCycleManagementReport.new user } + + describe "fetching orders" do + let(:supplier) { create(:supplier_enterprise) } + let(:product) { create(:simple_product, supplier: supplier) } + let(:order) { create(:order, completed_at: 1.day.ago) } + + it "only shows orders managed by the current user" do + d1 = create(:distributor_enterprise) + d1.enterprise_roles.build(user: user).save + d2 = create(:distributor_enterprise) + d2.enterprise_roles.build(user: create(:user)).save + + o1 = create(:order, distributor: d1, completed_at: 1.day.ago) + o2 = create(:order, distributor: d2, completed_at: 1.day.ago) + + subject.should_receive(:filter).with([o1]).and_return([o1]) + subject.orders.should == [o1] + end + + + it "does not show orders through a hub that the current user does not manage" do + # Given a supplier enterprise with an order for one of its products + supplier.enterprise_roles.build(user: user).save + order.line_items << create(:line_item, product: product) + + # When I fetch orders, I should see no orders + subject.should_receive(:filter).with([]).and_return([]) + subject.orders.should == [] + end + end + + describe "filtering orders" do + let(:orders) { Spree::Order.scoped } + let(:supplier) { create(:supplier_enterprise) } + + it "returns all orders sans-params" do + subject.filter(orders).should == orders + end + + it "filters to a specific order cycle" do + oc1 = create(:simple_order_cycle) + oc2 = create(:simple_order_cycle) + order1 = create(:order, order_cycle: oc1) + order2 = create(:order, order_cycle: oc2) + + subject.stub(:params).and_return(order_cycle_id: oc1.id) + subject.filter(orders).should == [order1] + end + + it "filters to a payment method" do + pm1 = create(:payment_method, name: "PM1") + pm2 = create(:payment_method, name: "PM2") + order1 = create(:order) + order2 = create(:order) + payment1 = create(:payment, :order => order1, :payment_method => pm1) + payment2 = create(:payment, :order => order2, :payment_method => pm2) + + subject.stub(:params).and_return(payment_method_name: pm1.name) + subject.filter(orders).should == [order1] + end + + it "filters to a shipping method" do + sm1 = create(:shipping_method, name: "ship1") + sm2 = create(:shipping_method, name: "ship2") + order1 = create(:order, shipping_method: sm1) + order2 = create(:order, shipping_method: sm2) + + subject.stub(:params).and_return(distribution_name: sm1.name) + subject.filter(orders).should == [order1] + end + + it "should do all the filters at once" do + pm1 = create(:payment_method, name: "PM1") + sm1 = create(:shipping_method, name: "ship1") + oc1 = create(:simple_order_cycle) + order1 = create(:order, order_cycle: oc1,shipping_method: sm1) + payment1 = create(:payment, :order => order1, :payment_method => pm1) + + subject.stub(:params).and_return( + order_cycle_id: oc1.id, + distribution_name: sm1.name, + payment_method_name: pm1.name) + subject.filter(orders) + + end + + + end + end + end +end From e607c9855f861e4fd359c9efb17254d473695496 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Dec 2014 10:59:23 +1100 Subject: [PATCH 324/681] Add Pin Payments gateway --- app/models/spree/gateway/pin.rb | 18 ++++++++++++++++++ app/models/spree/payment_method_decorator.rb | 2 ++ config/application.rb | 1 + spec/models/spree/payment_method_spec.rb | 1 + 4 files changed, 22 insertions(+) create mode 100644 app/models/spree/gateway/pin.rb diff --git a/app/models/spree/gateway/pin.rb b/app/models/spree/gateway/pin.rb new file mode 100644 index 0000000000..8cb3f73e85 --- /dev/null +++ b/app/models/spree/gateway/pin.rb @@ -0,0 +1,18 @@ +module Spree + class Gateway::Pin < Gateway + preference :api_key, :string + + attr_accessible :preferred_api_key + + + def provider_class + ActiveMerchant::Billing::PinGateway + end + + def options_with_test_preference + options_without_test_preference.merge(:test => self.preferred_test_mode) + end + + alias_method_chain :options, :test_preference + end +end diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index fd513cc0d3..915eb65ae8 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -46,6 +46,8 @@ Spree::Gateway.providers.each do |p| "Cash/EFT/etc. (payments for which automatic validation is not required)" when "Spree::Gateway::Migs" "MasterCard Internet Gateway Service (MIGS)" + when "Spree::Gateway::Pin" + "Pin Payments" when "Spree::Gateway::PayPalExpress" "PayPal Express" else diff --git a/config/application.rb b/config/application.rb index 885902e8f4..2d9dcd4982 100644 --- a/config/application.rb +++ b/config/application.rb @@ -39,6 +39,7 @@ module Openfoodnetwork # Register Spree payment methods initializer "spree.gateway.payment_methods", :after => "spree.register.payment_methods" do |app| app.config.spree.payment_methods << Spree::Gateway::Migs + app.config.spree.payment_methods << Spree::Gateway::Pin end # Settings in config/environments/* take precedence over those specified here. diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index 692db7c7a4..b61fd3bffd 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -19,6 +19,7 @@ module Spree it "generates a clean name for known Payment Method types" do Spree::PaymentMethod::Check.clean_name.should == "Cash/EFT/etc. (payments for which automatic validation is not required)" Spree::Gateway::Migs.clean_name.should == "MasterCard Internet Gateway Service (MIGS)" + Spree::Gateway::Pin.clean_name.should == "Pin Payments" Spree::Gateway::PayPalExpress.clean_name.should == "PayPal Express" # Testing else condition From 086f69ccfb703f882d646fb14167aaf23aeacf6f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Dec 2014 11:35:18 +1100 Subject: [PATCH 325/681] Default payment method server/test_mode to live --- app/models/spree/gateway_decorator.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/spree/gateway_decorator.rb b/app/models/spree/gateway_decorator.rb index e40b260ae8..3c6bb2638d 100644 --- a/app/models/spree/gateway_decorator.rb +++ b/app/models/spree/gateway_decorator.rb @@ -7,4 +7,9 @@ Spree::Gateway.class_eval do # To avoid that, we redefine this association here. has_and_belongs_to_many :distributors, join_table: 'distributors_payment_methods', :class_name => 'Enterprise', foreign_key: 'payment_method_id', association_foreign_key: 'distributor_id' + + + # Default to live + preference :server, :string, :default => 'live' + preference :test_mode, :boolean, :default => false end From 6a226e4f92abe17287c904e11f9ed681ac15900d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 12 Dec 2014 11:59:17 +1100 Subject: [PATCH 326/681] Fixing typos and changing delivery address display in order confirmation email --- app/models/spree/address_decorator.rb | 13 +++++-- .../confirm_email_for_customer.html.haml | 38 +++++++++++-------- .../confirm_email_for_shop.html.haml | 38 +++++++++++-------- spec/models/spree/addresses_spec.rb | 36 ++++++++++++++---- 4 files changed, 81 insertions(+), 44 deletions(-) diff --git a/app/models/spree/address_decorator.rb b/app/models/spree/address_decorator.rb index 8797a91599..03fa4da0cd 100644 --- a/app/models/spree/address_decorator.rb +++ b/app/models/spree/address_decorator.rb @@ -4,16 +4,21 @@ Spree::Address.class_eval do after_save :touch_enterprise - geocoded_by :full_address + geocoded_by :geocode_address delegate :name, :to => :state, :prefix => true, :allow_nil => true - def full_address - full_address = [address1, address2, zipcode, city, country.andand.name, state.andand.name] - filtered_address = full_address.select{ |field| !field.nil? && field != '' } + def geocode_address + geocode_address = [address1, address2, zipcode, city, country.andand.name, state.andand.name] + filtered_address = geocode_address.select{ |field| !field.nil? && field != '' } filtered_address.compact.join(', ') end + def full_address + full_address = [address1, address2, city, zipcode, state.andand.name] + filtered_address = full_address.select{ |field| !field.nil? && field != '' } + filtered_address.compact.join(', ') + end private diff --git a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml index 49f23087ed..69629e256b 100644 --- a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml @@ -10,7 +10,7 @@ %p Thanks for shopping on %strong= "#{Spree::Config.site_name}." - Here are the details for you order from + Here are the details for your order from %strong= "#{@order.distributor.name}." %table.column{:align => "left"} %tr @@ -77,24 +77,28 @@ %p Your order will be delivered to: %br - #{@order.ship_address.to_s} + #{@order.ship_address.full_name} %br + #{@order.ship_address.full_address} + %br + #{@order.ship_address.phone} - if @order.shipping_method.andand.description + %br + %br #{@order.shipping_method.description.html_safe} - %br - %br - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + %br + %br Delivery on: #{@order.order_cycle.pickup_time_for(@order.distributor)} - %br - %br - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) + %br + %br Other delivery information: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - %br - %br + %br - else / Collection details @@ -103,24 +107,26 @@ Collection details %p - if @order.shipping_method.andand.description + %br + %br = @order.shipping_method.description.html_safe - %br - %br - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + %br + %br Ready for collection: #{@order.order_cycle.pickup_time_for(@order.distributor)} - %br - %br - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) + %br + %br Collection instructions: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - %br - %br - if @order.special_instructions.present? + %br + %br Notes: #{@order.special_instructions} - %br - %br + + %br -# Your order will be ready for collection on -# %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} diff --git a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml index 49f23087ed..69629e256b 100644 --- a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml @@ -10,7 +10,7 @@ %p Thanks for shopping on %strong= "#{Spree::Config.site_name}." - Here are the details for you order from + Here are the details for your order from %strong= "#{@order.distributor.name}." %table.column{:align => "left"} %tr @@ -77,24 +77,28 @@ %p Your order will be delivered to: %br - #{@order.ship_address.to_s} + #{@order.ship_address.full_name} %br + #{@order.ship_address.full_address} + %br + #{@order.ship_address.phone} - if @order.shipping_method.andand.description + %br + %br #{@order.shipping_method.description.html_safe} - %br - %br - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + %br + %br Delivery on: #{@order.order_cycle.pickup_time_for(@order.distributor)} - %br - %br - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) + %br + %br Other delivery information: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - %br - %br + %br - else / Collection details @@ -103,24 +107,26 @@ Collection details %p - if @order.shipping_method.andand.description + %br + %br = @order.shipping_method.description.html_safe - %br - %br - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + %br + %br Ready for collection: #{@order.order_cycle.pickup_time_for(@order.distributor)} - %br - %br - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) + %br + %br Collection instructions: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - %br - %br - if @order.special_instructions.present? + %br + %br Notes: #{@order.special_instructions} - %br - %br + + %br -# Your order will be ready for collection on -# %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} diff --git a/spec/models/spree/addresses_spec.rb b/spec/models/spree/addresses_spec.rb index 6c6a88ba11..5fc9d442b6 100644 --- a/spec/models/spree/addresses_spec.rb +++ b/spec/models/spree/addresses_spec.rb @@ -9,23 +9,43 @@ describe Spree::Address do it { should delegate(:name).to(:state).with_prefix } end - describe "full address" do + describe "geocode address" do let(:address) { FactoryGirl.build(:address) } it "should include address1, address2, zipcode, city, state and country" do - address.full_address.should include(address.address1) - address.full_address.should include(address.address2) - address.full_address.should include(address.zipcode) - address.full_address.should include(address.city) - address.full_address.should include(address.state.name) - address.full_address.should include(address.country.name) + address.geocode_address.should include(address.address1) + address.geocode_address.should include(address.address2) + address.geocode_address.should include(address.zipcode) + address.geocode_address.should include(address.city) + address.geocode_address.should include(address.state.name) + address.geocode_address.should include(address.country.name) end it "should not include empty fields" do address.address2 = nil address.city = "" - address.full_address.split(',').length.should eql(4) + address.geocode_address.split(',').length.should eql(4) + end + end + + describe "full address" do + let(:address) { FactoryGirl.build(:address) } + + it "should include address1, address2, zipcode, city and state" do + address.full_address.should include(address.address1) + address.full_address.should include(address.address2) + address.full_address.should include(address.zipcode) + address.full_address.should include(address.city) + address.full_address.should include(address.state.name) + address.full_address.should_not include(address.country.name) + end + + it "should not include empty fields" do + address.address2 = nil + address.city = "" + + address.full_address.split(',').length.should eql(3) end end From 3a9c4e08268694231ce5be6e1aa6beb6c35a7676 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 12 Dec 2014 11:59:45 +1100 Subject: [PATCH 327/681] Adding a price without fees column to the order cycle customer report --- app/controllers/spree/admin/reports_controller_decorator.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 0aa41dbc05..02aba3fe9a 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -475,7 +475,7 @@ Spree::Admin::ReportsController.class_eval do table_items = @line_items @include_blank = 'All' - header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item ($)", "Dist ($)", "Ship ($)", "Total ($)", "Paid?", + header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item ($)", "Item + Fees ($)", "Dist ($)", "Ship ($)", "Total ($)", "Paid?", "Shipping", "Delivery?", "Ship street", "Ship street 2", "Ship city", "Ship postcode", "Ship state", "Order notes"] rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address } @@ -488,6 +488,7 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| line_items.first.variant.product.name }, proc { |line_items| line_items.first.variant.full_name }, proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.sum { |li| li.amount } }, proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, proc { |line_items| "" }, proc { |line_items| "" }, @@ -516,6 +517,7 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| "TOTAL" }, proc { |line_items| "" }, proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.amount } }, proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.admin_and_handling_total } }, proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, From 5b82fcaca70044c64ea85a4ff82a4a2469c25f9c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 21 Nov 2014 16:05:27 +1100 Subject: [PATCH 328/681] Inject hub permissions - which producers each hub can add to order cycle --- .../admin/products_controller_decorator.rb | 4 +++ app/helpers/admin/injection_helper.rb | 4 +++ .../override_variants/_data.html.haml | 1 + lib/open_food_network/permissions.rb | 10 ++++++ spec/features/admin/override_variants_spec.rb | 8 +++++ .../lib/open_food_network/permissions_spec.rb | 31 +++++++++++++++++++ 6 files changed, 58 insertions(+) diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index d44df1ffb5..4d18b5500a 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -51,6 +51,10 @@ Spree::Admin::ProductsController.class_eval do def override_variants @hubs = order_cycle_hub_enterprises(without_validation: true) + + @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). + order_cycle_enterprises_per_hub + @producers = order_cycle_producer_enterprises end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 94ee23347a..524bbb5a03 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -33,6 +33,10 @@ module Admin admin_inject_json_ams_array "ofn.admin", "producers", @producers, Api::Admin::IdNameSerializer end + def admin_inject_hub_permissions + render partial: "admin/json/injection_ams", locals: {ngModule: "ofn.admin", name: "hubPermissions", json: @hub_permissions.to_json} + end + def admin_inject_products admin_inject_json_ams_array "ofn.admin", "products", @products, Spree::Api::ProductSerializer end diff --git a/app/views/spree/admin/products/override_variants/_data.html.haml b/app/views/spree/admin/products/override_variants/_data.html.haml index d96db0edb9..6d04309e4d 100644 --- a/app/views/spree/admin/products/override_variants/_data.html.haml +++ b/app/views/spree/admin/products/override_variants/_data.html.haml @@ -1,3 +1,4 @@ = admin_inject_spree_api_key = admin_inject_hubs += admin_inject_hub_permissions = admin_inject_producers diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 476db23692..86f0bf3b39 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -15,6 +15,16 @@ module OpenFoodNetwork managed_and_related_enterprises_with :add_to_order_cycle end + def order_cycle_enterprises_per_hub + Hash[ + EnterpriseRelationship. + permitting(managed_enterprises). + with_permission(:add_to_order_cycle). + group_by { |er| er.child_id }. + map { |child_id, ers| [child_id, ers.map { |er| er.parent_id }] } + ] + end + # Find the exchanges of an order cycle that an admin can manage def order_cycle_exchanges(order_cycle) enterprises = managed_enterprises + related_enterprises_with(:add_to_order_cycle) diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb index 0372fed47d..1b6169a40f 100644 --- a/spec/features/admin/override_variants_spec.rb +++ b/spec/features/admin/override_variants_spec.rb @@ -17,6 +17,8 @@ feature %q{ let!(:hub) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } + let!(:er) { create(:enterprise_relationship, parent: producer, child: hub, + permissions_list: [:add_to_order_cycle]) } describe "selecting a hub" do it "displays a list of hub choices" do @@ -35,6 +37,8 @@ feature %q{ context "when a hub is selected" do let!(:product) { create(:simple_product, supplier: producer, price: 1.23, on_hand: 12) } + let!(:producer2) { create(:supplier_enterprise) } + let!(:product2) { create(:simple_product, supplier: producer2, price: 1.23, on_hand: 12) } before do visit '/admin/products/override_variants' @@ -47,6 +51,10 @@ feature %q{ page.should have_table_row [producer.name, product.name, '1.23', '12'] end + it "filters the products to those the hub can add to an order cycle" do + page.should_not have_table_row [producer2.name, product2.name, '1.23', '12'] + end + it "products values are affected by overrides" end end diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index bcd1f8b742..478629b822 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -21,6 +21,37 @@ module OpenFoodNetwork end end + describe "finding enterprises that can be added to an order cycle, for each hub" do + let!(:hub) { create(:distributor_enterprise) } + let!(:producer) { create(:supplier_enterprise) } + let!(:er) { create(:enterprise_relationship, parent: producer, child: hub, + permissions_list: [:add_to_order_cycle]) } + + before { permissions.stub(:managed_enterprises) { [hub] } } + + it "returns enterprises as hub_id => [producer, ...]" do + permissions.order_cycle_enterprises_per_hub.should == + {hub.id => [producer.id]} + end + + it "returns only permissions relating to managed enterprises" do + create(:enterprise_relationship, parent: e1, child: e2, + permissions_list: [:add_to_order_cycle]) + + permissions.order_cycle_enterprises_per_hub.should == + {hub.id => [producer.id]} + end + + it "returns only add_to_order_cycle permissions" do + permissions.stub(:managed_enterprises) { [hub, e2] } + create(:enterprise_relationship, parent: e1, child: e2, + permissions_list: [:manage_products]) + + permissions.order_cycle_enterprises_per_hub.should == + {hub.id => [producer.id]} + end + end + describe "finding exchanges of an order cycle that an admin can manage" do let(:oc) { create(:simple_order_cycle) } let!(:ex) { create(:exchange, order_cycle: oc, sender: e1, receiver: e2) } From b1ba519a73f6fe1d1c3d9849c5e1b5038c99cbac Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 26 Nov 2014 09:45:17 +1100 Subject: [PATCH 329/681] When finding per hub order cycle enterprises, also return managed producers --- lib/open_food_network/permissions.rb | 16 +++++++++++++++- spec/lib/open_food_network/permissions_spec.rb | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 86f0bf3b39..ca87f92b4e 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -15,14 +15,28 @@ module OpenFoodNetwork managed_and_related_enterprises_with :add_to_order_cycle end + # For every hub that an admin manages, show all the producers that that hub may add + # to the order cycle + # {hub1_id => [producer1_id, producer2_id, ...], ...} def order_cycle_enterprises_per_hub - Hash[ + permissions = Hash[ EnterpriseRelationship. permitting(managed_enterprises). with_permission(:add_to_order_cycle). group_by { |er| er.child_id }. map { |child_id, ers| [child_id, ers.map { |er| er.parent_id }] } ] + + managed_producer_ids = managed_enterprises.is_primary_producer.pluck(:id) + if managed_producer_ids.any? + managed_enterprises.is_distributor.each do |hub| + permissions[hub.id] ||= [] + permissions[hub.id] += managed_producer_ids + permissions[hub.id].uniq! + end + end + + permissions end # Find the exchanges of an order cycle that an admin can manage diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index 478629b822..6ce369fbb5 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -27,7 +27,9 @@ module OpenFoodNetwork let!(:er) { create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:add_to_order_cycle]) } - before { permissions.stub(:managed_enterprises) { [hub] } } + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: hub.id) } + end it "returns enterprises as hub_id => [producer, ...]" do permissions.order_cycle_enterprises_per_hub.should == @@ -43,13 +45,21 @@ module OpenFoodNetwork end it "returns only add_to_order_cycle permissions" do - permissions.stub(:managed_enterprises) { [hub, e2] } + permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub, e2]) } create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:manage_products]) permissions.order_cycle_enterprises_per_hub.should == {hub.id => [producer.id]} end + + it "also returns managed producers" do + producer2 = create(:supplier_enterprise) + permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub, producer2]) } + + permissions.order_cycle_enterprises_per_hub.should == + {hub.id => [producer.id, producer2.id]} + end end describe "finding exchanges of an order cycle that an admin can manage" do From 500b5ce347ff3fb4bc5a9300bb208d4c562e4fa2 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 26 Nov 2014 11:45:50 +1100 Subject: [PATCH 330/681] Enterprise managers can access override variants --- app/models/spree/ability_decorator.rb | 3 ++- spec/models/spree/ability_spec.rb | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 3a0466f1cc..6371d26aff 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -65,9 +65,10 @@ class AbilityDecorator def add_product_management_abilities(user) # Enterprise User can only access products that they are a supplier for can [:create], Spree::Product - can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], Spree::Product do |product| + can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :override_variants, :clone, :destroy], Spree::Product do |product| OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier end + can :override_variants, nil can [:create], Spree::Variant can [:admin, :index, :read, :edit, :update, :search, :destroy], Spree::Variant do |variant| diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 4a8f34a1d9..1662ab7942 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -140,21 +140,25 @@ module Spree let(:order) {create(:order)} it "should be able to read/write their enterprises' products and variants" do - should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p1) + should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :override_variants, :clone, :destroy], for: p1) should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p1.master) end it "should be able to read/write related enterprises' products and variants with manage_products permission" do er_p - should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p_related) + should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :override_variants, :clone, :destroy], for: p_related) should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p_related.master) end it "should not be able to read/write other enterprises' products and variants" do - should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p2) + should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :override_variants, :clone, :destroy], for: p2) should_not have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p2.master) end + it "should be able to override_variants on nil (required for override_variants)" do + should have_ability :override_variants, for: nil + end + it "should not be able to access admin actions on orders" do should_not have_ability([:admin], for: Spree::Order) end From a3a3832c8dab56c14bd44c3ff4f068c52648ab6a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 26 Nov 2014 11:51:52 +1100 Subject: [PATCH 331/681] Only show products that the chosen hub can add to an order cycle --- .../override_variants_controller.js.coffee | 4 +- .../filters/hub_permissions_filter.js.coffee | 4 + .../override_variants/_products.html.haml | 2 +- spec/features/admin/override_variants_spec.rb | 73 ++++++++++--------- 4 files changed, 44 insertions(+), 39 deletions(-) create mode 100644 app/assets/javascripts/admin/filters/hub_permissions_filter.js.coffee diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 871cbb9b03..d4bd2d73ba 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,9 +1,9 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, hubs, producers) -> +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, hubs, producers, hubPermissions) -> $scope.hubs = hubs $scope.hub = null $scope.products = [] $scope.producers = Indexer.index producers - + $scope.hubPermissions = hubPermissions $scope.initialise = -> SpreeApiAuth.authorise() diff --git a/app/assets/javascripts/admin/filters/hub_permissions_filter.js.coffee b/app/assets/javascripts/admin/filters/hub_permissions_filter.js.coffee new file mode 100644 index 0000000000..5db7a6d40e --- /dev/null +++ b/app/assets/javascripts/admin/filters/hub_permissions_filter.js.coffee @@ -0,0 +1,4 @@ +angular.module("ofn.admin").filter "hubPermissions", ($filter) -> + return (products, hubPermissions, hub_id) -> + return [] if !hub_id + return $filter('filter')(products, ((product) -> hubPermissions[hub_id].indexOf(product.producer_id) > -1), true) diff --git a/app/views/spree/admin/products/override_variants/_products.html.haml b/app/views/spree/admin/products/override_variants/_products.html.haml index f5081e54bd..6388656654 100644 --- a/app/views/spree/admin/products/override_variants/_products.html.haml +++ b/app/views/spree/admin/products/override_variants/_products.html.haml @@ -6,7 +6,7 @@ %th Price %th On hand %tbody - %tr{ng: {repeat: 'product in products'}} + %tr{ng: {repeat: 'product in products | hubPermissions:hubPermissions:hub.id'}} %td {{ producers[product.producer_id].name }} %td {{ product.name }} %td {{ product.price }} diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb index 1b6169a40f..1ae981a8aa 100644 --- a/spec/features/admin/override_variants_spec.rb +++ b/spec/features/admin/override_variants_spec.rb @@ -9,10 +9,6 @@ feature %q{ include AuthenticationWorkflow include WebHelper - before do - login_to_admin_section - end - use_short_wait let!(:hub) { create(:distributor_enterprise) } @@ -20,41 +16,46 @@ feature %q{ let!(:er) { create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:add_to_order_cycle]) } - describe "selecting a hub" do - it "displays a list of hub choices" do - visit '/admin/products/override_variants' - page.should have_select2 'hub_id', options: ['', hub.name] + context "as an enterprise user" do + let(:user) { create_enterprise_user enterprises: [hub, producer] } + before { quick_login_as user } + + describe "selecting a hub" do + it "displays a list of hub choices" do + visit '/admin/products/override_variants' + page.should have_select2 'hub_id', options: ['', hub.name] + end + + it "displays the hub" do + visit '/admin/products/override_variants' + select2_select hub.name, from: 'hub_id' + click_button 'Go' + + page.should have_selector 'h2', text: hub.name + end end - it "displays the hub" do - visit '/admin/products/override_variants' - select2_select hub.name, from: 'hub_id' - click_button 'Go' + context "when a hub is selected" do + let!(:product) { create(:simple_product, supplier: producer, price: 1.23, on_hand: 12) } + let!(:producer2) { create(:supplier_enterprise) } + let!(:product2) { create(:simple_product, supplier: producer2, price: 1.23, on_hand: 12) } - page.should have_selector 'h2', text: hub.name + before do + visit '/admin/products/override_variants' + select2_select hub.name, from: 'hub_id' + click_button 'Go' + end + + it "displays the list of products" do + page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] + page.should have_table_row [producer.name, product.name, '1.23', '12'] + end + + it "filters the products to those the hub can add to an order cycle" do + page.should_not have_table_row [producer2.name, product2.name, '1.23', '12'] + end + + it "products values are affected by overrides" end end - - context "when a hub is selected" do - let!(:product) { create(:simple_product, supplier: producer, price: 1.23, on_hand: 12) } - let!(:producer2) { create(:supplier_enterprise) } - let!(:product2) { create(:simple_product, supplier: producer2, price: 1.23, on_hand: 12) } - - before do - visit '/admin/products/override_variants' - select2_select hub.name, from: 'hub_id' - click_button 'Go' - end - - it "displays the list of products" do - page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] - page.should have_table_row [producer.name, product.name, '1.23', '12'] - end - - it "filters the products to those the hub can add to an order cycle" do - page.should_not have_table_row [producer2.name, product2.name, '1.23', '12'] - end - - it "products values are affected by overrides" - end end From 91b386003adec8c781e238e90810c38a1c9128bf Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 26 Nov 2014 12:35:23 +1100 Subject: [PATCH 332/681] Add VariantOverrideSerializer --- .../api/admin/variant_override_serializer.rb | 3 +++ .../admin/variant_override_serializer_spec.rb | 15 +++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 app/serializers/api/admin/variant_override_serializer.rb create mode 100644 spec/serializers/admin/variant_override_serializer_spec.rb diff --git a/app/serializers/api/admin/variant_override_serializer.rb b/app/serializers/api/admin/variant_override_serializer.rb new file mode 100644 index 0000000000..bc057adc78 --- /dev/null +++ b/app/serializers/api/admin/variant_override_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::VariantOverrideSerializer < ActiveModel::Serializer + attributes :id, :variant_id, :hub_id, :price, :count_on_hand +end diff --git a/spec/serializers/admin/variant_override_serializer_spec.rb b/spec/serializers/admin/variant_override_serializer_spec.rb new file mode 100644 index 0000000000..80158b93cf --- /dev/null +++ b/spec/serializers/admin/variant_override_serializer_spec.rb @@ -0,0 +1,15 @@ +describe Api::Admin::VariantOverrideSerializer do + let(:variant) { create(:variant) } + let(:hub) { create(:distributor_enterprise) } + let(:price) { 77.77 } + let(:count_on_hand) { 11111 } + let(:variant_override) { create(:variant_override, variant: variant, hub: hub, price: price, count_on_hand: count_on_hand) } + + it "serializes a variant override" do + serializer = Api::Admin::VariantOverrideSerializer.new variant_override + serializer.to_json.should match variant.id.to_s + serializer.to_json.should match hub.id.to_s + serializer.to_json.should match price.to_s + serializer.to_json.should match count_on_hand.to_s + end +end From ff28da345d57dcd81023ef8d23043ee68c856bfd Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 26 Nov 2014 13:07:18 +1100 Subject: [PATCH 333/681] Find variant overrides for some hubs --- app/models/variant_override.rb | 4 ++++ spec/models/variant_override_spec.rb | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index 6d20a95571..4c6c29a5ec 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -2,6 +2,10 @@ class VariantOverride < ActiveRecord::Base belongs_to :variant, class_name: 'Spree::Variant' belongs_to :hub, class_name: 'Enterprise' + scope :for_hubs, lambda { |hubs| + where(hub_id: hubs) + } + def self.price_for(variant, hub) VariantOverride.where(variant_id: variant, hub_id: hub).first.andand.price end diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb index 093ed6007e..082a7fb135 100644 --- a/spec/models/variant_override_spec.rb +++ b/spec/models/variant_override_spec.rb @@ -1,6 +1,18 @@ require 'spec_helper' describe VariantOverride do + describe "scopes" do + let(:hub1) { create(:distributor_enterprise) } + let(:hub2) { create(:distributor_enterprise) } + let(:v) { create(:variant) } + let!(:vo1) { create(:variant_override, hub: hub1, variant: v) } + let!(:vo2) { create(:variant_override, hub: hub2, variant: v) } + + it "finds variant overrides for a set of hubs" do + VariantOverride.for_hubs([hub1, hub2]).sort.should == [vo1, vo2].sort + end + end + describe "looking up prices" do let(:variant) { create(:variant) } let(:hub) { create(:distributor_enterprise) } From efae9265c12b6bb942fb39007caf633c08d099b0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 26 Nov 2014 13:08:16 +1100 Subject: [PATCH 334/681] Inject variant overrides --- app/controllers/spree/admin/products_controller_decorator.rb | 5 ++--- app/helpers/admin/injection_helper.rb | 4 ++++ .../spree/admin/products/override_variants/_data.html.haml | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 4d18b5500a..00742bdbec 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -51,11 +51,10 @@ Spree::Admin::ProductsController.class_eval do def override_variants @hubs = order_cycle_hub_enterprises(without_validation: true) - + @producers = order_cycle_producer_enterprises @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). order_cycle_enterprises_per_hub - - @producers = order_cycle_producer_enterprises + @variant_overrides = VariantOverride.for_hubs(@hubs) end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 524bbb5a03..f01f9b9b01 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -49,6 +49,10 @@ module Admin admin_inject_json_ams_array "ofn.admin", "users", @users, Api::Admin::UserSerializer end + def admin_inject_variant_overrides + admin_inject_json_ams_array "ofn.admin", "variantOverrides", @variant_overrides, Api::Admin::VariantOverrideSerializer + end + def admin_inject_spree_api_key render partial: "admin/json/injection_ams", locals: {ngModule: 'ofn.admin', name: 'SpreeApiKey', json: "'#{@spree_api_key.to_s}'"} end diff --git a/app/views/spree/admin/products/override_variants/_data.html.haml b/app/views/spree/admin/products/override_variants/_data.html.haml index 6d04309e4d..3b5f7f125c 100644 --- a/app/views/spree/admin/products/override_variants/_data.html.haml +++ b/app/views/spree/admin/products/override_variants/_data.html.haml @@ -2,3 +2,4 @@ = admin_inject_hubs = admin_inject_hub_permissions = admin_inject_producers += admin_inject_variant_overrides From 42481e96914d527ad90f258a86d6dcd50451b940 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 27 Nov 2014 10:00:28 +1100 Subject: [PATCH 335/681] Provide hub permissions to specs --- .../controllers/override_variants_controller_spec.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee index f3c796cd51..a34fa61816 100644 --- a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee @@ -4,6 +4,7 @@ describe "OverrideVariantsCtrl", -> hubs = [{id: 1, name: 'Hub'}] producers = [{id: 2, name: 'Producer'}] products = [{id: 1, name: 'Product'}] + hubPermissions = {} beforeEach -> module 'ofn.admin' @@ -13,7 +14,7 @@ describe "OverrideVariantsCtrl", -> scope = {} inject ($controller, Indexer) -> - ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products} + ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions} it "initialises the hub list and the chosen hub", -> expect(scope.hubs).toEqual hubs From 47b479c6c9c36f8d4c8517f3243ef60e32e31cc7 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 27 Nov 2014 10:48:20 +1100 Subject: [PATCH 336/681] Show variants in override variants interface, not just products --- .../override_variants/_products.html.haml | 10 ++++---- .../_products_product.html.haml | 5 ++++ .../_products_variants.html.haml | 5 ++++ spec/features/admin/override_variants_spec.rb | 23 +++++++++++++------ 4 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 app/views/spree/admin/products/override_variants/_products_product.html.haml create mode 100644 app/views/spree/admin/products/override_variants/_products_variants.html.haml diff --git a/app/views/spree/admin/products/override_variants/_products.html.haml b/app/views/spree/admin/products/override_variants/_products.html.haml index 6388656654..3be701d203 100644 --- a/app/views/spree/admin/products/override_variants/_products.html.haml +++ b/app/views/spree/admin/products/override_variants/_products.html.haml @@ -5,9 +5,7 @@ %th Product %th Price %th On hand - %tbody - %tr{ng: {repeat: 'product in products | hubPermissions:hubPermissions:hub.id'}} - %td {{ producers[product.producer_id].name }} - %td {{ product.name }} - %td {{ product.price }} - %td {{ product.on_hand }} + %tbody{ng: {repeat: 'product in products | hubPermissions:hubPermissions:hub.id'}} + = render 'spree/admin/products/override_variants/products_product' + = render 'spree/admin/products/override_variants/products_variants' + diff --git a/app/views/spree/admin/products/override_variants/_products_product.html.haml b/app/views/spree/admin/products/override_variants/_products_product.html.haml new file mode 100644 index 0000000000..ccb3b97f6a --- /dev/null +++ b/app/views/spree/admin/products/override_variants/_products_product.html.haml @@ -0,0 +1,5 @@ +%tr.product + %td {{ producers[product.producer_id].name }} + %td {{ product.name }} + %td + %td diff --git a/app/views/spree/admin/products/override_variants/_products_variants.html.haml b/app/views/spree/admin/products/override_variants/_products_variants.html.haml new file mode 100644 index 0000000000..b15127677a --- /dev/null +++ b/app/views/spree/admin/products/override_variants/_products_variants.html.haml @@ -0,0 +1,5 @@ +%tr.variant{ng: {repeat: 'variant in product.variants'}} + %td + %td {{ variant.options_text }} + %td {{ variant.price }} + %td {{ variant.on_hand }} diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb index 1ae981a8aa..1a37e90a84 100644 --- a/spec/features/admin/override_variants_spec.rb +++ b/spec/features/admin/override_variants_spec.rb @@ -12,18 +12,19 @@ feature %q{ use_short_wait let!(:hub) { create(:distributor_enterprise) } + let!(:hub2) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } let!(:er) { create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:add_to_order_cycle]) } context "as an enterprise user" do - let(:user) { create_enterprise_user enterprises: [hub, producer] } + let(:user) { create_enterprise_user enterprises: [hub, hub2, producer] } before { quick_login_as user } describe "selecting a hub" do it "displays a list of hub choices" do visit '/admin/products/override_variants' - page.should have_select2 'hub_id', options: ['', hub.name] + page.should have_select2 'hub_id', options: ['', hub.name, hub2.name] end it "displays the hub" do @@ -36,23 +37,31 @@ feature %q{ end context "when a hub is selected" do - let!(:product) { create(:simple_product, supplier: producer, price: 1.23, on_hand: 12) } + let!(:product) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } + let!(:variant) { create(:variant, product: product, unit_value: 1, price: 1.23, on_hand: 12) } let!(:producer2) { create(:supplier_enterprise) } - let!(:product2) { create(:simple_product, supplier: producer2, price: 1.23, on_hand: 12) } + let!(:product2) { create(:simple_product, supplier: producer2) } + let!(:er) { create(:enterprise_relationship, parent: producer2, child: hub2, + permissions_list: [:add_to_order_cycle]) } before do + # Remove 'S' option value + variant.option_values.first.destroy + visit '/admin/products/override_variants' select2_select hub.name, from: 'hub_id' click_button 'Go' end - it "displays the list of products" do + it "displays the list of products with variants" do page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] - page.should have_table_row [producer.name, product.name, '1.23', '12'] + page.should have_table_row [producer.name, product.name, '', ''] + page.should have_table_row ['', '1g', '1.23', '12'] end it "filters the products to those the hub can add to an order cycle" do - page.should_not have_table_row [producer2.name, product2.name, '1.23', '12'] + page.should_not have_content producer2.name + page.should_not have_content product2.name end it "products values are affected by overrides" From 8baed4429c5df713de422804c8eac6c9d2d8cb52 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 27 Nov 2014 11:34:35 +1100 Subject: [PATCH 337/681] Indexer accepts arbitrary key to index by. Fix bug: Return an object instead of an array. --- app/assets/javascripts/admin/services/indexer.js.coffee | 6 +++--- .../unit/admin/services/indexer_spec.js.coffee | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/services/indexer.js.coffee b/app/assets/javascripts/admin/services/indexer.js.coffee index a69f7f06e1..f9a9688a2f 100644 --- a/app/assets/javascripts/admin/services/indexer.js.coffee +++ b/app/assets/javascripts/admin/services/indexer.js.coffee @@ -6,8 +6,8 @@ angular.module("ofn.admin").factory 'Indexer', -> new class Indexer - index: (data) -> - index = [] + index: (data, key='id') -> + index = {} for e in data - index[e.id] = e + index[e[key]] = e index diff --git a/spec/javascripts/unit/admin/services/indexer_spec.js.coffee b/spec/javascripts/unit/admin/services/indexer_spec.js.coffee index a7f009e7d3..f17f8bd83c 100644 --- a/spec/javascripts/unit/admin/services/indexer_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/indexer_spec.js.coffee @@ -11,3 +11,12 @@ describe "indexer", -> objects = [{id: 1, name: 'one'}, {id: 2, name: 'two'}] index = Indexer.index objects expect(index).toEqual({1: {id: 1, name: 'one'}, 2: {id: 2, name: 'two'}}) + + it "indexes an array of objects by another field", -> + objects = [{widget_id: 1, name: 'one'}, {widget_id: 2, name: 'two'}] + index = Indexer.index objects, 'widget_id' + expect(index).toEqual({1: {widget_id: 1, name: 'one'}, 2: {widget_id: 2, name: 'two'}}) + + it "returns an object, not an array", -> + index = Indexer.index [] + expect(index.constructor).not.toEqual(Array) From 3aedbb6c48adbdacc595ea43710e58f52875e8e1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 27 Nov 2014 11:45:49 +1100 Subject: [PATCH 338/681] Display variant overrides --- .../override_variants_controller.js.coffee | 3 +- .../_products_variants.html.haml | 4 +- spec/factories.rb | 5 +++ spec/features/admin/override_variants_spec.rb | 42 +++++++++++++------ ...verride_variants_controller_spec.js.coffee | 3 +- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index d4bd2d73ba..2d4c55e356 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,9 +1,10 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, hubs, producers, hubPermissions) -> +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, hubs, producers, hubPermissions, variantOverrides) -> $scope.hubs = hubs $scope.hub = null $scope.products = [] $scope.producers = Indexer.index producers $scope.hubPermissions = hubPermissions + $scope.variantOverrides = Indexer.index variantOverrides, 'variant_id' $scope.initialise = -> SpreeApiAuth.authorise() diff --git a/app/views/spree/admin/products/override_variants/_products_variants.html.haml b/app/views/spree/admin/products/override_variants/_products_variants.html.haml index b15127677a..73dca63650 100644 --- a/app/views/spree/admin/products/override_variants/_products_variants.html.haml +++ b/app/views/spree/admin/products/override_variants/_products_variants.html.haml @@ -1,5 +1,5 @@ %tr.variant{ng: {repeat: 'variant in product.variants'}} %td %td {{ variant.options_text }} - %td {{ variant.price }} - %td {{ variant.on_hand }} + %td {{ variant.price }} - {{ variantOverrides[variant.id].price }} + %td {{ variant.on_hand }} - {{ variantOverrides[variant.id].count_on_hand }} diff --git a/spec/factories.rb b/spec/factories.rb index 0112ea8b51..214c61962d 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -89,6 +89,11 @@ FactoryGirl.define do incoming false end + factory :variant_override, :class => VariantOverride do + price 77.77 + count_on_hand 11111 + end + factory :enterprise, :class => Enterprise do owner { FactoryGirl.create :user } sequence(:name) { |n| "Enterprise #{n}" } diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb index 1a37e90a84..a62c6fbc33 100644 --- a/spec/features/admin/override_variants_spec.rb +++ b/spec/features/admin/override_variants_spec.rb @@ -47,24 +47,40 @@ feature %q{ before do # Remove 'S' option value variant.option_values.first.destroy - - visit '/admin/products/override_variants' - select2_select hub.name, from: 'hub_id' - click_button 'Go' end - it "displays the list of products with variants" do - page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] - page.should have_table_row [producer.name, product.name, '', ''] - page.should have_table_row ['', '1g', '1.23', '12'] + context "with no overrides" do + before do + visit '/admin/products/override_variants' + select2_select hub.name, from: 'hub_id' + click_button 'Go' + end + + it "displays the list of products with variants" do + page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] + page.should have_table_row [producer.name, product.name, '', ''] + page.should have_table_row ['', '1g', '1.23 -', '12 -'] + end + + it "filters the products to those the hub can add to an order cycle" do + page.should_not have_content producer2.name + page.should_not have_content product2.name + end end - it "filters the products to those the hub can add to an order cycle" do - page.should_not have_content producer2.name - page.should_not have_content product2.name - end + context "with overrides" do + let!(:override) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111) } - it "products values are affected by overrides" + before do + visit '/admin/products/override_variants' + select2_select hub.name, from: 'hub_id' + click_button 'Go' + end + + it "product values are affected by overrides" do + page.should have_table_row ['', '1g', '1.23 - 77.77', '12 - 11111'] + end + end end end end diff --git a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee index a34fa61816..093ccc3cb2 100644 --- a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee @@ -5,6 +5,7 @@ describe "OverrideVariantsCtrl", -> producers = [{id: 2, name: 'Producer'}] products = [{id: 1, name: 'Product'}] hubPermissions = {} + variantOverrides = {} beforeEach -> module 'ofn.admin' @@ -14,7 +15,7 @@ describe "OverrideVariantsCtrl", -> scope = {} inject ($controller, Indexer) -> - ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions} + ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions, variantOverrides: variantOverrides} it "initialises the hub list and the chosen hub", -> expect(scope.hubs).toEqual hubs From 5df4f1b7fe32cab1a82cc4b8a5831a2b500d2c2f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 10:27:45 +1100 Subject: [PATCH 339/681] Convert text to form fields --- .../override_variants/_products_variants.html.haml | 7 +++++-- spec/features/admin/override_variants_spec.rb | 6 ++++-- spec/support/request/web_helper.rb | 10 ++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/views/spree/admin/products/override_variants/_products_variants.html.haml b/app/views/spree/admin/products/override_variants/_products_variants.html.haml index 73dca63650..75c1ad7cad 100644 --- a/app/views/spree/admin/products/override_variants/_products_variants.html.haml +++ b/app/views/spree/admin/products/override_variants/_products_variants.html.haml @@ -1,5 +1,8 @@ %tr.variant{ng: {repeat: 'variant in product.variants'}} %td %td {{ variant.options_text }} - %td {{ variant.price }} - {{ variantOverrides[variant.id].price }} - %td {{ variant.on_hand }} - {{ variantOverrides[variant.id].count_on_hand }} + %td + %input{name: 'variant-overrides-{{ variant.id }}-price', type: 'text', ng: {model: 'variantOverrides[variant.id].price'}, placeholder: '{{ variant.price }}'} + + %td + %input{name: 'variant-overrides-{{ variant.id }}-count-on-hand', type: 'text', ng: {model: 'variantOverrides[variant.id].count_on_hand'}, placeholder: '{{ variant.on_hand }}'} diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb index a62c6fbc33..810cb1e0a9 100644 --- a/spec/features/admin/override_variants_spec.rb +++ b/spec/features/admin/override_variants_spec.rb @@ -59,7 +59,8 @@ feature %q{ it "displays the list of products with variants" do page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] page.should have_table_row [producer.name, product.name, '', ''] - page.should have_table_row ['', '1g', '1.23 -', '12 -'] + page.should have_input "variant-overrides-#{variant.id}-price", placeholder: '1.23' + page.should have_input "variant-overrides-#{variant.id}-count-on-hand", placeholder: '12' end it "filters the products to those the hub can add to an order cycle" do @@ -78,7 +79,8 @@ feature %q{ end it "product values are affected by overrides" do - page.should have_table_row ['', '1g', '1.23 - 77.77', '12 - 11111'] + page.should have_input "variant-overrides-#{variant.id}-price", with: '77.77', placeholder: '1.23' + page.should have_input "variant-overrides-#{variant.id}-count-on-hand", with: '11111', placeholder: '12' end end end diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index a4f3b5fd00..bf76ff9919 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -16,6 +16,16 @@ module WebHelper end end + def have_input(name, opts={}) + selector = "[name='#{name}']" + selector += "[placeholder='#{opts[:placeholder]}']" if opts.key? :placeholder + + element = page.all(selector).first + element.value.should == opts[:with] if element && opts.key?(:with) + + have_selector selector + end + def current_path_should_be path current_path = URI.parse(current_url).path From 5cf3579b264117001a6afc2e268ab7a5be95b378 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 11:50:39 +1100 Subject: [PATCH 340/681] Extract variant overrides to service --- .../controllers/override_variants_controller.js.coffee | 4 ++-- .../javascripts/admin/services/variant_overrides.js.coffee | 6 ++++++ .../controllers/override_variants_controller_spec.js.coffee | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/admin/services/variant_overrides.js.coffee diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 2d4c55e356..8f0919af56 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,10 +1,10 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, hubs, producers, hubPermissions, variantOverrides) -> +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, hubs, producers, hubPermissions, VariantOverrides) -> $scope.hubs = hubs $scope.hub = null $scope.products = [] $scope.producers = Indexer.index producers $scope.hubPermissions = hubPermissions - $scope.variantOverrides = Indexer.index variantOverrides, 'variant_id' + $scope.variantOverrides = VariantOverrides.variantOverrides $scope.initialise = -> SpreeApiAuth.authorise() diff --git a/app/assets/javascripts/admin/services/variant_overrides.js.coffee b/app/assets/javascripts/admin/services/variant_overrides.js.coffee new file mode 100644 index 0000000000..363fa4a8bc --- /dev/null +++ b/app/assets/javascripts/admin/services/variant_overrides.js.coffee @@ -0,0 +1,6 @@ +angular.module("ofn.admin").factory "VariantOverrides", (variantOverrides, Indexer) -> + new class VariantOverrides + variantOverrides: {} + + constructor: -> + @variantOverrides = Indexer.index variantOverrides, 'variant_id' diff --git a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee index 093ccc3cb2..b9389d132f 100644 --- a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee @@ -11,11 +11,12 @@ describe "OverrideVariantsCtrl", -> module 'ofn.admin' module ($provide) -> $provide.value 'SpreeApiKey', 'API_KEY' + $provide.value 'variantOverrides', variantOverrides null scope = {} - inject ($controller, Indexer) -> - ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions, variantOverrides: variantOverrides} + inject ($controller, Indexer, VariantOverrides) -> + ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions, VariantOverrides: VariantOverrides} it "initialises the hub list and the chosen hub", -> expect(scope.hubs).toEqual hubs From d3e639aa03a7d91fea1ec7ff63c5d83ec4da3e8a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 12:12:20 +1100 Subject: [PATCH 341/681] Index variant overrides by hub_id x variant_id --- .../services/variant_overrides.js.coffee | 4 +++- .../_products_variants.html.haml | 4 ++-- .../services/variant_overrides_spec.js.coffee | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee diff --git a/app/assets/javascripts/admin/services/variant_overrides.js.coffee b/app/assets/javascripts/admin/services/variant_overrides.js.coffee index 363fa4a8bc..56307bdbc8 100644 --- a/app/assets/javascripts/admin/services/variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/services/variant_overrides.js.coffee @@ -3,4 +3,6 @@ angular.module("ofn.admin").factory "VariantOverrides", (variantOverrides, Index variantOverrides: {} constructor: -> - @variantOverrides = Indexer.index variantOverrides, 'variant_id' + for vo in variantOverrides + @variantOverrides[vo.hub_id] ||= {} + @variantOverrides[vo.hub_id][vo.variant_id] = vo diff --git a/app/views/spree/admin/products/override_variants/_products_variants.html.haml b/app/views/spree/admin/products/override_variants/_products_variants.html.haml index 75c1ad7cad..371f0db30e 100644 --- a/app/views/spree/admin/products/override_variants/_products_variants.html.haml +++ b/app/views/spree/admin/products/override_variants/_products_variants.html.haml @@ -2,7 +2,7 @@ %td %td {{ variant.options_text }} %td - %input{name: 'variant-overrides-{{ variant.id }}-price', type: 'text', ng: {model: 'variantOverrides[variant.id].price'}, placeholder: '{{ variant.price }}'} + %input{name: 'variant-overrides-{{ variant.id }}-price', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].price'}, placeholder: '{{ variant.price }}'} %td - %input{name: 'variant-overrides-{{ variant.id }}-count-on-hand', type: 'text', ng: {model: 'variantOverrides[variant.id].count_on_hand'}, placeholder: '{{ variant.on_hand }}'} + %input{name: 'variant-overrides-{{ variant.id }}-count-on-hand', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].count_on_hand'}, placeholder: '{{ variant.on_hand }}'} diff --git a/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee b/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee new file mode 100644 index 0000000000..01cf73aeda --- /dev/null +++ b/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee @@ -0,0 +1,24 @@ +describe "VariantOverrides service", -> + VariantOverrides = null + variantOverrides = [ + {id: 1, hub_id: 10, variant_id: 100, price: 1, count_on_hand: 1} + {id: 2, hub_id: 10, variant_id: 200, price: 2, count_on_hand: 2} + {id: 3, hub_id: 20, variant_id: 300, price: 3, count_on_hand: 3} + ] + + beforeEach -> + module "ofn.admin" + module ($provide) -> + $provide.value "variantOverrides", variantOverrides + null + + beforeEach inject (_VariantOverrides_) -> + VariantOverrides = _VariantOverrides_ + + it "indexes variant overrides by hub_id -> variant_id", -> + expect(VariantOverrides.variantOverrides).toEqual + 10: + 100: {id: 1, hub_id: 10, variant_id: 100, price: 1, count_on_hand: 1} + 200: {id: 2, hub_id: 10, variant_id: 200, price: 2, count_on_hand: 2} + 20: + 300: {id: 3, hub_id: 20, variant_id: 300, price: 3, count_on_hand: 3} From abf58c0e02b2876fafe088d728ccc2f855bf7fff Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 15:22:54 +1100 Subject: [PATCH 342/681] Provide blank values for all variant overrides --- .../override_variants_controller.js.coffee | 1 + .../services/variant_overrides.js.coffee | 11 +++++++ ...verride_variants_controller_spec.js.coffee | 10 +++++-- .../services/variant_overrides_spec.js.coffee | 29 +++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 8f0919af56..89edca5745 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -22,6 +22,7 @@ angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Ind $scope.addProducts = (products) -> $scope.products = $scope.products.concat products + VariantOverrides.ensureDataFor hubs, products $scope.selectHub = -> diff --git a/app/assets/javascripts/admin/services/variant_overrides.js.coffee b/app/assets/javascripts/admin/services/variant_overrides.js.coffee index 56307bdbc8..69a55e9f35 100644 --- a/app/assets/javascripts/admin/services/variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/services/variant_overrides.js.coffee @@ -6,3 +6,14 @@ angular.module("ofn.admin").factory "VariantOverrides", (variantOverrides, Index for vo in variantOverrides @variantOverrides[vo.hub_id] ||= {} @variantOverrides[vo.hub_id][vo.variant_id] = vo + + ensureDataFor: (hubs, products) -> + for hub in hubs + @variantOverrides[hub.id] ||= {} + for product in products + for variant in product.variants + @variantOverrides[hub.id][variant.id] ||= + variant_id: variant.id + hub_id: hub.id + price: '' + count_on_hand: '' diff --git a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee index b9389d132f..7ab4c177dc 100644 --- a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee @@ -5,6 +5,7 @@ describe "OverrideVariantsCtrl", -> producers = [{id: 2, name: 'Producer'}] products = [{id: 1, name: 'Product'}] hubPermissions = {} + VariantOverrides = null variantOverrides = {} beforeEach -> @@ -15,19 +16,22 @@ describe "OverrideVariantsCtrl", -> null scope = {} - inject ($controller, Indexer, VariantOverrides) -> - ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions, VariantOverrides: VariantOverrides} + inject ($controller, Indexer, _VariantOverrides_) -> + VariantOverrides = _VariantOverrides_ + ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions, VariantOverrides: _VariantOverrides_} it "initialises the hub list and the chosen hub", -> expect(scope.hubs).toEqual hubs - expect(scope.hub).toBeNull + expect(scope.hub).toBeNull() it "adds products", -> + spyOn(VariantOverrides, "ensureDataFor") expect(scope.products).toEqual [] scope.addProducts ['a', 'b'] expect(scope.products).toEqual ['a', 'b'] scope.addProducts ['c', 'd'] expect(scope.products).toEqual ['a', 'b', 'c', 'd'] + expect(VariantOverrides.ensureDataFor).toHaveBeenCalled() describe "selecting a hub", -> it "sets the chosen hub", -> diff --git a/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee b/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee index 01cf73aeda..7bd0605de4 100644 --- a/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee @@ -22,3 +22,32 @@ describe "VariantOverrides service", -> 200: {id: 2, hub_id: 10, variant_id: 200, price: 2, count_on_hand: 2} 20: 300: {id: 3, hub_id: 20, variant_id: 300, price: 3, count_on_hand: 3} + + it "ensures blank data available for some products", -> + hubs = [{id: 10}, {id: 20}, {id: 30}] + products = [ + { + id: 1 + variants: [{id: 100}, {id: 200}, {id: 300}, {id: 400}, {id: 500}] + } + ] + VariantOverrides.ensureDataFor hubs, products + expect(VariantOverrides.variantOverrides).toEqual + 10: + 100: {id: 1, hub_id: 10, variant_id: 100, price: 1, count_on_hand: 1} + 200: {id: 2, hub_id: 10, variant_id: 200, price: 2, count_on_hand: 2} + 300: { hub_id: 10, variant_id: 300, price: '', count_on_hand: ''} + 400: { hub_id: 10, variant_id: 400, price: '', count_on_hand: ''} + 500: { hub_id: 10, variant_id: 500, price: '', count_on_hand: ''} + 20: + 100: { hub_id: 20, variant_id: 100, price: '', count_on_hand: ''} + 200: { hub_id: 20, variant_id: 200, price: '', count_on_hand: ''} + 300: {id: 3, hub_id: 20, variant_id: 300, price: 3, count_on_hand: 3} + 400: { hub_id: 20, variant_id: 400, price: '', count_on_hand: ''} + 500: { hub_id: 20, variant_id: 500, price: '', count_on_hand: ''} + 30: + 100: { hub_id: 30, variant_id: 100, price: '', count_on_hand: ''} + 200: { hub_id: 30, variant_id: 200, price: '', count_on_hand: ''} + 300: { hub_id: 30, variant_id: 300, price: '', count_on_hand: ''} + 400: { hub_id: 30, variant_id: 400, price: '', count_on_hand: ''} + 500: { hub_id: 30, variant_id: 500, price: '', count_on_hand: ''} From bcc3815f6bcf4cab440eaddde546a7b318d58237 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 17:07:51 +1100 Subject: [PATCH 343/681] Extract status message to a service --- .../admin/bulk_product_update.js.coffee | 28 ++++++------------- .../admin/services/status_message.js.coffee | 26 +++++++++++++++++ .../products/bulk_edit/_actions.html.haml | 4 +-- 3 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 app/assets/javascripts/admin/services/status_message.js.coffee diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index ddd3bda162..0a35c8a678 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,9 +1,7 @@ -angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons, SpreeApiAuth) -> +angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth) -> $scope.loading = true - $scope.updateStatusMessage = - text: "" - style: {} + $scope.StatusMessage = StatusMessage $scope.columns = producer: {name: "Producer", visible: true} @@ -64,7 +62,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.resetProducts = -> DirtyProducts.clear() - $scope.setMessage $scope.updateStatusMessage, "", {}, false + StatusMessage.clearMessage() # $scope.matchProducer = (product) -> # for producer in $scope.producers @@ -182,7 +180,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout if productsToSubmit.length > 0 $scope.updateProducts productsToSubmit # Don't submit an empty list else - $scope.setMessage $scope.updateStatusMessage, "No changes to save.", color: "grey", 3000 + StatusMessage.displayMessage 'No changes to save.', 'alert' $scope.updateProducts = (productsToSubmit) -> @@ -250,31 +248,23 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.displayUpdating = -> - $scope.setMessage $scope.updateStatusMessage, "Saving...", - color: "#FF9906" - , false + StatusMessage.displayMessage 'Saving...', 'progress' $scope.displaySuccess = -> - $scope.setMessage $scope.updateStatusMessage, "Changes saved.", - color: "#9fc820" - , 3000 + StatusMessage.displayMessage 'Changes saved.', 'success' $scope.displayFailure = (failMessage) -> - $scope.setMessage $scope.updateStatusMessage, "Saving failed. " + failMessage, - color: "#DA5354" - , false + StatusMessage.displayMessage "Saving failed. #{failMessage}", 'failure' $scope.displayDirtyProducts = -> if DirtyProducts.count() > 0 message = if DirtyProducts.count() == 1 then "one product" else DirtyProducts.count() + " products" - $scope.setMessage $scope.updateStatusMessage, "Changes to " + message + " remain unsaved.", - color: "gray" - , false + StatusMessage.displayMessage "Changes to #{message} remain unsaved.", 'notice' else - $scope.setMessage $scope.updateStatusMessage, "", {}, false + StatusMessage.clearMessage() filterSubmitProducts = (productsToFilter) -> diff --git a/app/assets/javascripts/admin/services/status_message.js.coffee b/app/assets/javascripts/admin/services/status_message.js.coffee new file mode 100644 index 0000000000..a400f0428e --- /dev/null +++ b/app/assets/javascripts/admin/services/status_message.js.coffee @@ -0,0 +1,26 @@ +angular.module("ofn.admin").factory "StatusMessage", ($timeout) -> + new class StatusMessage + types: + progress: {timeout: false, style: {color: '#ff9906'}} + alert: {timeout: 3000, style: {color: 'grey'}} + notice: {timeout: false, style: {color: 'grey'}} + success: {timeout: 3000, style: {color: '#9fc820'}} + failure: {timeout: false, style: {color: '#da5354'}} + + statusMessage: + text: "" + style: {} + + displayMessage: (text, type) -> + @statusMessage.text = text + @statusMessage.style = @types[type].style + $timeout.cancel @statusMessage.timeout if @statusMessage.timeout + timeout = @types[type].timeout + if timeout + @statusMessage.timeout = $timeout => + @clearMessage() + , timeout, true + + clearMessage: -> + @statusMessage.text = '' + @statusMessage.style = {} diff --git a/app/views/spree/admin/products/bulk_edit/_actions.html.haml b/app/views/spree/admin/products/bulk_edit/_actions.html.haml index d86f16bc09..92265953d8 100644 --- a/app/views/spree/admin/products/bulk_edit/_actions.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_actions.html.haml @@ -2,8 +2,8 @@ %div.four.columns.alpha %input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'} %div.nine.columns - %h6{ id: "update-status-message", ng: { style: 'updateStatusMessage.style' } } - {{ updateStatusMessage.text || " " }} + %h6{ id: "update-status-message", ng: { style: 'StatusMessage.statusMessage.style' } } + {{ StatusMessage.statusMessage.text || " " }} %div.three.columns.omega %div.ofn_drop_down.three.columns.omega{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' } %span{ :class => 'icon-reorder' }   Columns From 9c4c0f87e380087ab4dfbfb6e31b6842a128331d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 17:09:05 +1100 Subject: [PATCH 344/681] Rename methods to reduce naming duplication --- .../admin/bulk_product_update.js.coffee | 14 +++++++------- .../admin/services/status_message.js.coffee | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 0a35c8a678..c93b8447e2 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -62,7 +62,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.resetProducts = -> DirtyProducts.clear() - StatusMessage.clearMessage() + StatusMessage.clear() # $scope.matchProducer = (product) -> # for producer in $scope.producers @@ -180,7 +180,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout if productsToSubmit.length > 0 $scope.updateProducts productsToSubmit # Don't submit an empty list else - StatusMessage.displayMessage 'No changes to save.', 'alert' + StatusMessage.display 'No changes to save.', 'alert' $scope.updateProducts = (productsToSubmit) -> @@ -248,23 +248,23 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.displayUpdating = -> - StatusMessage.displayMessage 'Saving...', 'progress' + StatusMessage.display 'Saving...', 'progress' $scope.displaySuccess = -> - StatusMessage.displayMessage 'Changes saved.', 'success' + StatusMessage.display 'Changes saved.', 'success' $scope.displayFailure = (failMessage) -> - StatusMessage.displayMessage "Saving failed. #{failMessage}", 'failure' + StatusMessage.display "Saving failed. #{failMessage}", 'failure' $scope.displayDirtyProducts = -> if DirtyProducts.count() > 0 message = if DirtyProducts.count() == 1 then "one product" else DirtyProducts.count() + " products" - StatusMessage.displayMessage "Changes to #{message} remain unsaved.", 'notice' + StatusMessage.display "Changes to #{message} remain unsaved.", 'notice' else - StatusMessage.clearMessage() + StatusMessage.clear() filterSubmitProducts = (productsToFilter) -> diff --git a/app/assets/javascripts/admin/services/status_message.js.coffee b/app/assets/javascripts/admin/services/status_message.js.coffee index a400f0428e..3019b2e7cf 100644 --- a/app/assets/javascripts/admin/services/status_message.js.coffee +++ b/app/assets/javascripts/admin/services/status_message.js.coffee @@ -11,16 +11,16 @@ angular.module("ofn.admin").factory "StatusMessage", ($timeout) -> text: "" style: {} - displayMessage: (text, type) -> + display: (text, type) -> @statusMessage.text = text @statusMessage.style = @types[type].style $timeout.cancel @statusMessage.timeout if @statusMessage.timeout timeout = @types[type].timeout if timeout @statusMessage.timeout = $timeout => - @clearMessage() + @clear() , timeout, true - clearMessage: -> + clear: -> @statusMessage.text = '' @statusMessage.style = {} From a8e1265a5809b217d3aa7b0de1a439daf12fcd51 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 17:14:28 +1100 Subject: [PATCH 345/681] Swap parameter ordering - follows convention Rails.logger.error 'foo' --- .../javascripts/admin/bulk_product_update.js.coffee | 10 +++++----- .../admin/services/status_message.js.coffee | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index c93b8447e2..a2bbc3bb03 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -180,7 +180,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout if productsToSubmit.length > 0 $scope.updateProducts productsToSubmit # Don't submit an empty list else - StatusMessage.display 'No changes to save.', 'alert' + StatusMessage.display 'alert', 'No changes to save.' $scope.updateProducts = (productsToSubmit) -> @@ -248,21 +248,21 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.displayUpdating = -> - StatusMessage.display 'Saving...', 'progress' + StatusMessage.display 'progress', 'Saving...' $scope.displaySuccess = -> - StatusMessage.display 'Changes saved.', 'success' + StatusMessage.display 'success', 'Changes saved.' $scope.displayFailure = (failMessage) -> - StatusMessage.display "Saving failed. #{failMessage}", 'failure' + StatusMessage.display 'failure', "Saving failed. #{failMessage}" $scope.displayDirtyProducts = -> if DirtyProducts.count() > 0 message = if DirtyProducts.count() == 1 then "one product" else DirtyProducts.count() + " products" - StatusMessage.display "Changes to #{message} remain unsaved.", 'notice' + StatusMessage.display 'notice', "Changes to #{message} remain unsaved." else StatusMessage.clear() diff --git a/app/assets/javascripts/admin/services/status_message.js.coffee b/app/assets/javascripts/admin/services/status_message.js.coffee index 3019b2e7cf..914dabb390 100644 --- a/app/assets/javascripts/admin/services/status_message.js.coffee +++ b/app/assets/javascripts/admin/services/status_message.js.coffee @@ -11,7 +11,7 @@ angular.module("ofn.admin").factory "StatusMessage", ($timeout) -> text: "" style: {} - display: (text, type) -> + display: (type, text) -> @statusMessage.text = text @statusMessage.style = @types[type].style $timeout.cancel @statusMessage.timeout if @statusMessage.timeout From 0938debafc6aa249f201c8e37d449a35ea38cb84 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 17:24:00 +1100 Subject: [PATCH 346/681] Extract status message markup into partial --- app/views/spree/admin/products/bulk_edit/_actions.html.haml | 1 + app/views/spree/admin/shared/_status_message.html.haml | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 app/views/spree/admin/shared/_status_message.html.haml diff --git a/app/views/spree/admin/products/bulk_edit/_actions.html.haml b/app/views/spree/admin/products/bulk_edit/_actions.html.haml index 92265953d8..96be4db424 100644 --- a/app/views/spree/admin/products/bulk_edit/_actions.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_actions.html.haml @@ -2,6 +2,7 @@ %div.four.columns.alpha %input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'} %div.nine.columns + = render 'spree/admin/shared/status_message' %h6{ id: "update-status-message", ng: { style: 'StatusMessage.statusMessage.style' } } {{ StatusMessage.statusMessage.text || " " }} %div.three.columns.omega diff --git a/app/views/spree/admin/shared/_status_message.html.haml b/app/views/spree/admin/shared/_status_message.html.haml new file mode 100644 index 0000000000..fc611dd928 --- /dev/null +++ b/app/views/spree/admin/shared/_status_message.html.haml @@ -0,0 +1,2 @@ +%h6{ id: "update-status-message", ng: { style: 'StatusMessage.statusMessage.style' } } + {{ StatusMessage.statusMessage.text || " " }} From 9d4b8ae94970b1662c725068527cdcf0af70a8fc Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 17:26:57 +1100 Subject: [PATCH 347/681] Rename div update-status-message to status-message --- .../stylesheets/admin/products.css.scss | 2 +- .../products/bulk_edit/_actions.html.haml | 2 +- .../admin/shared/_status_message.html.haml | 2 +- .../admin/bulk_product_update_spec.rb | 28 +++++++++---------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index bddc7f9e9e..b14dadd866 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -23,7 +23,7 @@ th.left-actions, td.left-actions { border-right: 1px solid #cee1f4 !important; } -#update-status-message { +#status-message { margin: 4px 0px; font-weight: bold; } diff --git a/app/views/spree/admin/products/bulk_edit/_actions.html.haml b/app/views/spree/admin/products/bulk_edit/_actions.html.haml index 96be4db424..3c93fa2c0a 100644 --- a/app/views/spree/admin/products/bulk_edit/_actions.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_actions.html.haml @@ -3,7 +3,7 @@ %input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'} %div.nine.columns = render 'spree/admin/shared/status_message' - %h6{ id: "update-status-message", ng: { style: 'StatusMessage.statusMessage.style' } } + %h6{ id: "status-message", ng: { style: 'StatusMessage.statusMessage.style' } } {{ StatusMessage.statusMessage.text || " " }} %div.three.columns.omega %div.ofn_drop_down.three.columns.omega{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' } diff --git a/app/views/spree/admin/shared/_status_message.html.haml b/app/views/spree/admin/shared/_status_message.html.haml index fc611dd928..f9de868721 100644 --- a/app/views/spree/admin/shared/_status_message.html.haml +++ b/app/views/spree/admin/shared/_status_message.html.haml @@ -1,2 +1,2 @@ -%h6{ id: "update-status-message", ng: { style: 'StatusMessage.statusMessage.style' } } +%h6{ id: "status-message", ng: { style: 'StatusMessage.statusMessage.style' } } {{ StatusMessage.statusMessage.text || " " }} diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 65776c4845..debb234284 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -267,7 +267,7 @@ feature %q{ fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." updated_variant = Spree::Variant.where(deleted_at: nil).last expect(updated_variant.display_name).to eq "Case of 12 Bottles" @@ -320,7 +320,7 @@ feature %q{ end click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "Big Bag Of Potatoes" @@ -347,7 +347,7 @@ feature %q{ fill_in "variant_unit_name", with: "loaf" click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.variant_unit).to eq "items" @@ -370,7 +370,7 @@ feature %q{ fill_in "variant_unit_value_with_description", with: '123 abc' click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.variant_unit).to eq "weight" @@ -395,7 +395,7 @@ feature %q{ fill_in "master_unit_value_with_description", with: '123 abc' click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.variant_unit).to eq 'weight' @@ -443,7 +443,7 @@ feature %q{ expect(page).to have_selector "span[name='on_hand']", text: "10" click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." v.reload expect(v.price).to eq 4.0 @@ -467,7 +467,7 @@ feature %q{ fill_in "variant_price", with: "10.0" click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." v.reload expect(v.price).to eq 10.0 @@ -484,21 +484,21 @@ feature %q{ fill_in "product_name", with: "new name 1" click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "new name 1" fill_in "product_name", with: "new name 2" click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "new name 2" fill_in "product_name", with: "original name" click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "original name" end @@ -514,7 +514,7 @@ feature %q{ fill_in "product_name", :with => "new product name" click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "new product name" end @@ -527,7 +527,7 @@ feature %q{ visit '/admin/products/bulk_edit' click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "No changes to save." + expect(page.find("#status-message")).to have_content "No changes to save." end end @@ -546,7 +546,7 @@ feature %q{ fill_in "product_name", :with => "new product1" click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p1.reload expect(p1.name).to eq "new product1" end @@ -822,7 +822,7 @@ feature %q{ end click_button 'Save Changes' - expect(page.find("#update-status-message")).to have_content "Changes saved." + expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "Big Bag Of Potatoes" From 7255520471ba3618bd2679f8f09fef5519fd0631 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 4 Dec 2014 17:28:23 +1100 Subject: [PATCH 348/681] Display success message on save variant overrides (stub) --- .../controllers/override_variants_controller.js.coffee | 7 ++++++- app/views/spree/admin/products/override_variants.html.haml | 4 +++- .../admin/products/override_variants/_actions.html.haml | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 app/views/spree/admin/products/override_variants/_actions.html.haml diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 89edca5745..663c6b467f 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,10 +1,11 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, hubs, producers, hubPermissions, VariantOverrides) -> +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides) -> $scope.hubs = hubs $scope.hub = null $scope.products = [] $scope.producers = Indexer.index producers $scope.hubPermissions = hubPermissions $scope.variantOverrides = VariantOverrides.variantOverrides + $scope.StatusMessage = StatusMessage $scope.initialise = -> SpreeApiAuth.authorise() @@ -27,3 +28,7 @@ angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Ind $scope.selectHub = -> $scope.hub = (hub for hub in hubs when hub.id == $scope.hub_id)[0] + + + $scope.update = -> + StatusMessage.display 'success', 'Changes saved.' \ No newline at end of file diff --git a/app/views/spree/admin/products/override_variants.html.haml b/app/views/spree/admin/products/override_variants.html.haml index a3165f8850..4c4cc46b4f 100644 --- a/app/views/spree/admin/products/override_variants.html.haml +++ b/app/views/spree/admin/products/override_variants.html.haml @@ -4,6 +4,8 @@ %div{ ng: { app: 'ofn.admin', controller: 'AdminOverrideVariantsCtrl', init: 'initialise()' } } = render 'spree/admin/products/override_variants/hub_choice' - %h2{ng: {show: 'hub'}} {{ hub.name }} + %div{ng: {show: 'hub'}} + %h2 {{ hub.name }} + = render 'spree/admin/products/override_variants/actions' = render 'spree/admin/products/override_variants/products' diff --git a/app/views/spree/admin/products/override_variants/_actions.html.haml b/app/views/spree/admin/products/override_variants/_actions.html.haml new file mode 100644 index 0000000000..0ae6f8b96b --- /dev/null +++ b/app/views/spree/admin/products/override_variants/_actions.html.haml @@ -0,0 +1,4 @@ +.row + %input.four.columns.alpha{type: 'button', value: 'Save Changes', 'ng-click' => 'update()'} + .twelve.columns.omega + = render 'spree/admin/shared/status_message' From a1fc4dec43ab2f0e56be0d5e038bbddd7dc01a7d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 10:21:58 +1100 Subject: [PATCH 349/681] Updating variant overrides stores dirty values --- .../override_variants_controller.js.coffee | 3 ++ .../track_variant_override.js.coffee | 9 +++++ .../dirty_variant_overrides.js.coffee | 7 ++++ .../_products_variants.html.haml | 4 +- .../dirty_variant_overrides_spec.js.coffee | 40 +++++++++++++++++++ 5 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/admin/directives/track_variant_override.js.coffee create mode 100644 app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee create mode 100644 spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 663c6b467f..c84a1e1541 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -30,5 +30,8 @@ angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Ind $scope.hub = (hub for hub in hubs when hub.id == $scope.hub_id)[0] + $scope.displayDirty = -> + + $scope.update = -> StatusMessage.display 'success', 'Changes saved.' \ No newline at end of file diff --git a/app/assets/javascripts/admin/directives/track_variant_override.js.coffee b/app/assets/javascripts/admin/directives/track_variant_override.js.coffee new file mode 100644 index 0000000000..bb8117a757 --- /dev/null +++ b/app/assets/javascripts/admin/directives/track_variant_override.js.coffee @@ -0,0 +1,9 @@ +angular.module("ofn.admin").directive "ofnTrackVariantOverride", (DirtyVariantOverrides) -> + require: "ngModel" + link: (scope, element, attrs, ngModel) -> + ngModel.$parsers.push (viewValue) -> + if ngModel.$dirty + variantOverride = scope.variantOverrides[scope.hub.id][scope.variant.id] + DirtyVariantOverrides.add variantOverride + scope.displayDirty() + viewValue diff --git a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee new file mode 100644 index 0000000000..68a6a923b7 --- /dev/null +++ b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee @@ -0,0 +1,7 @@ +angular.module("ofn.admin").factory "DirtyVariantOverrides", -> + new class DirtyVariantOverrides + dirtyVariantOverrides: {} + + add: (vo) -> + @dirtyVariantOverrides[vo.hub_id] ||= {} + @dirtyVariantOverrides[vo.hub_id][vo.variant_id] = vo diff --git a/app/views/spree/admin/products/override_variants/_products_variants.html.haml b/app/views/spree/admin/products/override_variants/_products_variants.html.haml index 371f0db30e..8af837901e 100644 --- a/app/views/spree/admin/products/override_variants/_products_variants.html.haml +++ b/app/views/spree/admin/products/override_variants/_products_variants.html.haml @@ -2,7 +2,7 @@ %td %td {{ variant.options_text }} %td - %input{name: 'variant-overrides-{{ variant.id }}-price', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].price'}, placeholder: '{{ variant.price }}'} + %input{name: 'variant-overrides-{{ variant.id }}-price', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].price'}, placeholder: '{{ variant.price }}', 'ofn-track-variant-override' => 'price'} %td - %input{name: 'variant-overrides-{{ variant.id }}-count-on-hand', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].count_on_hand'}, placeholder: '{{ variant.on_hand }}'} + %input{name: 'variant-overrides-{{ variant.id }}-count-on-hand', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].count_on_hand'}, placeholder: '{{ variant.on_hand }}', 'ofn-track-variant-override' => 'price'} diff --git a/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee new file mode 100644 index 0000000000..bae955fd20 --- /dev/null +++ b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee @@ -0,0 +1,40 @@ +describe "maintaining a list of dirty variant overrides", -> + DirtyVariantOverrides = null + variantOverride = + variant_id: 1 + hub_id: 2 + price: 3 + count_on_hand: 4 + + beforeEach -> + module "ofn.admin" + + beforeEach inject (_DirtyVariantOverrides_) -> + DirtyVariantOverrides = _DirtyVariantOverrides_ + + it "adds new dirty variant overrides", -> + DirtyVariantOverrides.add variantOverride + expect(DirtyVariantOverrides.dirtyVariantOverrides).toEqual + 2: + 1: + variant_id: 1 + hub_id: 2 + price: 3 + count_on_hand: 4 + + it "updates existing dirty variant overrides", -> + DirtyVariantOverrides.dirtyVariantOverrides = + 2: + 1: + variant_id: 5 + hub_id: 6 + price: 7 + count_on_hand: 8 + DirtyVariantOverrides.add variantOverride + expect(DirtyVariantOverrides.dirtyVariantOverrides).toEqual + 2: + 1: + variant_id: 1 + hub_id: 2 + price: 3 + count_on_hand: 4 From c62ce57e0f23790fc5c600c411edae8b09df9546 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 10:37:52 +1100 Subject: [PATCH 350/681] Display count of dirty variant overrides --- .../override_variants_controller.js.coffee | 7 ++++++- .../dirty_variant_overrides.js.coffee | 6 ++++++ .../dirty_variant_overrides_spec.js.coffee | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index c84a1e1541..383b42843f 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides) -> +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides, DirtyVariantOverrides) -> $scope.hubs = hubs $scope.hub = null $scope.products = [] @@ -31,6 +31,11 @@ angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Ind $scope.displayDirty = -> + if DirtyVariantOverrides.count() > 0 + num = if DirtyVariantOverrides.count() == 1 then "one override" else "#{DirtyVariantOverrides.count()} overrides" + StatusMessage.display 'notice', "Changes to #{num} remain unsaved." + else + StatusMessage.clear() $scope.update = -> diff --git a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee index 68a6a923b7..10964b4199 100644 --- a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee @@ -5,3 +5,9 @@ angular.module("ofn.admin").factory "DirtyVariantOverrides", -> add: (vo) -> @dirtyVariantOverrides[vo.hub_id] ||= {} @dirtyVariantOverrides[vo.hub_id][vo.variant_id] = vo + + count: -> + count = 0 + for hub_id, vos of @dirtyVariantOverrides + count += Object.keys(vos).length + count \ No newline at end of file diff --git a/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee index bae955fd20..bdd0afd9d6 100644 --- a/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee @@ -38,3 +38,24 @@ describe "maintaining a list of dirty variant overrides", -> hub_id: 2 price: 3 count_on_hand: 4 + + it "counts dirty variant overrides", -> + DirtyVariantOverrides.dirtyVariantOverrides = + 2: + 1: + variant_id: 5 + hub_id: 6 + price: 7 + count_on_hand: 8 + 3: + variant_id: 9 + hub_id: 10 + price: 11 + count_on_hand: 12 + 4: + 5: + variant_id: 13 + hub_id: 14 + price: 15 + count_on_hand: 16 + expect(DirtyVariantOverrides.count()).toEqual 3 From 0a984b904127c9f3656b98a7dd3d90d6f0ada522 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 10:38:18 +1100 Subject: [PATCH 351/681] Remove duplicate status message on BPE --- .../admin/services/dirty_variant_overrides.js.coffee | 2 +- app/views/spree/admin/products/bulk_edit/_actions.html.haml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee index 10964b4199..ea85baa341 100644 --- a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee @@ -10,4 +10,4 @@ angular.module("ofn.admin").factory "DirtyVariantOverrides", -> count = 0 for hub_id, vos of @dirtyVariantOverrides count += Object.keys(vos).length - count \ No newline at end of file + count diff --git a/app/views/spree/admin/products/bulk_edit/_actions.html.haml b/app/views/spree/admin/products/bulk_edit/_actions.html.haml index 3c93fa2c0a..337f1d469b 100644 --- a/app/views/spree/admin/products/bulk_edit/_actions.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_actions.html.haml @@ -3,8 +3,6 @@ %input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'} %div.nine.columns = render 'spree/admin/shared/status_message' - %h6{ id: "status-message", ng: { style: 'StatusMessage.statusMessage.style' } } - {{ StatusMessage.statusMessage.text || " " }} %div.three.columns.omega %div.ofn_drop_down.three.columns.omega{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' } %span{ :class => 'icon-reorder' }   Columns From d67e61494781e4620766db9a8af3824620baa466 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 10:40:19 +1100 Subject: [PATCH 352/681] Remove extracted method --- .../javascripts/admin/bulk_product_update.js.coffee | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index a2bbc3bb03..e8e14413ae 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -237,15 +237,6 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout if $scope.limit < $scope.products.length $scope.limit = $scope.limit + 5 - $scope.setMessage = (model, text, style, timeout) -> - model.text = text - model.style = style - $timeout.cancel model.timeout if model.timeout - if timeout - model.timeout = $timeout(-> - $scope.setMessage model, "", {}, false - , timeout, true) - $scope.displayUpdating = -> StatusMessage.display 'progress', 'Saving...' From 47592cdbd8ec77bc7e8cf13d9e8c048ae1e584ef Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 11:14:21 +1100 Subject: [PATCH 353/681] Submit variant overrides to server --- .../override_variants_controller.js.coffee | 14 ++++- .../dirty_variant_overrides.js.coffee | 22 +++++++- .../dirty_variant_overrides_spec.js.coffee | 54 ++++++++++++------- 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 383b42843f..7eb60855ae 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides, DirtyVariantOverrides) -> +angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, $timeout, Indexer, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides, DirtyVariantOverrides) -> $scope.hubs = hubs $scope.hub = null $scope.products = [] @@ -39,4 +39,14 @@ angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Ind $scope.update = -> - StatusMessage.display 'success', 'Changes saved.' \ No newline at end of file + if DirtyVariantOverrides.count() == 0 + StatusMessage.display 'alert', 'No changes to save.' + else + StatusMessage.display 'progress', 'Saving...' + DirtyVariantOverrides.save + success: (data) -> + DirtyVariantOverrides.clear() + #VariantOverrides.update data.variant_overrides + $timeout -> StatusMessage.display 'success', 'Changes saved.' + error: (data, status) -> + $timeout -> StatusMessage.display 'failure', 'Oh no!' diff --git a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee index ea85baa341..f0f5b48549 100644 --- a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").factory "DirtyVariantOverrides", -> +angular.module("ofn.admin").factory "DirtyVariantOverrides", ($http) -> new class DirtyVariantOverrides dirtyVariantOverrides: {} @@ -11,3 +11,23 @@ angular.module("ofn.admin").factory "DirtyVariantOverrides", -> for hub_id, vos of @dirtyVariantOverrides count += Object.keys(vos).length count + + clear: -> + @dirtyVariantOverrides = {} + + all: -> + all_vos = [] + for hub_id, vos of @dirtyVariantOverrides + all_vos.push vo for variant_id, vo of vos + all_vos + + save: (callbacks={}) -> + $http + method: "POST" + url: "/admin/products/override_variants" + data: + variant_overrides: @all() + .success (data) -> + (callbacks.success || Angular.noop) data + .error (data, status) -> + (callbacks.error || Angular.noop) data, status diff --git a/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee index bdd0afd9d6..653560a989 100644 --- a/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee @@ -39,23 +39,37 @@ describe "maintaining a list of dirty variant overrides", -> price: 3 count_on_hand: 4 - it "counts dirty variant overrides", -> - DirtyVariantOverrides.dirtyVariantOverrides = - 2: - 1: - variant_id: 5 - hub_id: 6 - price: 7 - count_on_hand: 8 - 3: - variant_id: 9 - hub_id: 10 - price: 11 - count_on_hand: 12 - 4: - 5: - variant_id: 13 - hub_id: 14 - price: 15 - count_on_hand: 16 - expect(DirtyVariantOverrides.count()).toEqual 3 + describe "with a number of variant overrides", -> + beforeEach -> + DirtyVariantOverrides.dirtyVariantOverrides = + 2: + 1: + variant_id: 5 + hub_id: 6 + price: 7 + count_on_hand: 8 + 3: + variant_id: 9 + hub_id: 10 + price: 11 + count_on_hand: 12 + 4: + 5: + variant_id: 13 + hub_id: 14 + price: 15 + count_on_hand: 16 + + it "counts dirty variant overrides", -> + expect(DirtyVariantOverrides.count()).toEqual 3 + + it "clears dirty variant overrides", -> + DirtyVariantOverrides.clear() + expect(DirtyVariantOverrides.dirtyVariantOverrides).toEqual {} + + it "returns a flattened list of overrides", -> + expect(DirtyVariantOverrides.all()).toEqual [ + {variant_id: 5, hub_id: 6, price: 7, count_on_hand: 8} + {variant_id: 9, hub_id: 10, price: 11, count_on_hand: 12} + {variant_id: 13, hub_id: 14, price: 15, count_on_hand: 16} + ] From a1906a71fa8213b4d62696d56bd3509553597cd3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 11:22:40 +1100 Subject: [PATCH 354/681] Return promise directly --- .../override_variants_controller.js.coffee | 14 +++++++------- .../services/dirty_variant_overrides.js.coffee | 6 +----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 7eb60855ae..1d6fe64a70 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -43,10 +43,10 @@ angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, $ti StatusMessage.display 'alert', 'No changes to save.' else StatusMessage.display 'progress', 'Saving...' - DirtyVariantOverrides.save - success: (data) -> - DirtyVariantOverrides.clear() - #VariantOverrides.update data.variant_overrides - $timeout -> StatusMessage.display 'success', 'Changes saved.' - error: (data, status) -> - $timeout -> StatusMessage.display 'failure', 'Oh no!' + DirtyVariantOverrides.save() + .success (data) -> + DirtyVariantOverrides.clear() + #VariantOverrides.update data.variant_overrides + $timeout -> StatusMessage.display 'success', 'Changes saved.' + .error (data, status) -> + $timeout -> StatusMessage.display 'failure', 'Oh no!' diff --git a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee index f0f5b48549..14a1ff35f9 100644 --- a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee @@ -21,13 +21,9 @@ angular.module("ofn.admin").factory "DirtyVariantOverrides", ($http) -> all_vos.push vo for variant_id, vo of vos all_vos - save: (callbacks={}) -> + save: -> $http method: "POST" url: "/admin/products/override_variants" data: variant_overrides: @all() - .success (data) -> - (callbacks.success || Angular.noop) data - .error (data, status) -> - (callbacks.error || Angular.noop) data, status From 51d2805de9dc8bcd54cf5a0cbc98255fc89520e5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 11:49:56 +1100 Subject: [PATCH 355/681] WIP: Rename override_variants to variant_overrides --- .../admin/services/dirty_variant_overrides.js.coffee | 2 +- .../spree/admin/products_controller_decorator.rb | 4 ++-- app/models/spree/ability_decorator.rb | 4 ++-- .../add_override_variants_tab.html.haml.deface | 2 +- ..._variants.html.haml => variant_overrides.html.haml} | 0 .../_actions.html.haml | 0 .../_data.html.haml | 0 .../_header.html.haml | 0 .../_hub_choice.html.haml | 0 .../_products.html.haml | 0 .../_products_product.html.haml | 0 .../_products_variants.html.haml | 0 config/routes.rb | 2 +- spec/features/admin/override_variants_spec.rb | 8 ++++---- spec/models/spree/ability_spec.rb | 10 +++++----- 15 files changed, 16 insertions(+), 16 deletions(-) rename app/views/spree/admin/products/{override_variants.html.haml => variant_overrides.html.haml} (100%) rename app/views/spree/admin/products/{override_variants => variant_overrides}/_actions.html.haml (100%) rename app/views/spree/admin/products/{override_variants => variant_overrides}/_data.html.haml (100%) rename app/views/spree/admin/products/{override_variants => variant_overrides}/_header.html.haml (100%) rename app/views/spree/admin/products/{override_variants => variant_overrides}/_hub_choice.html.haml (100%) rename app/views/spree/admin/products/{override_variants => variant_overrides}/_products.html.haml (100%) rename app/views/spree/admin/products/{override_variants => variant_overrides}/_products_product.html.haml (100%) rename app/views/spree/admin/products/{override_variants => variant_overrides}/_products_variants.html.haml (100%) diff --git a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee index 14a1ff35f9..72455326a2 100644 --- a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee @@ -24,6 +24,6 @@ angular.module("ofn.admin").factory "DirtyVariantOverrides", ($http) -> save: -> $http method: "POST" - url: "/admin/products/override_variants" + url: "/admin/products/variant_overrides" data: variant_overrides: @all() diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 00742bdbec..feb64ecf9b 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -4,7 +4,7 @@ Spree::Admin::ProductsController.class_eval do include OpenFoodNetwork::SpreeApiKeyLoader include OrderCyclesHelper before_filter :load_form_data, :only => [:bulk_edit, :new, :create, :edit, :update] - before_filter :load_spree_api_key, :only => [:bulk_edit, :override_variants] + before_filter :load_spree_api_key, :only => [:bulk_edit, :variant_overrides] alias_method :location_after_save_original, :location_after_save @@ -49,7 +49,7 @@ Spree::Admin::ProductsController.class_eval do end end - def override_variants + def variant_overrides @hubs = order_cycle_hub_enterprises(without_validation: true) @producers = order_cycle_producer_enterprises @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 6371d26aff..97d4235b9a 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -65,10 +65,10 @@ class AbilityDecorator def add_product_management_abilities(user) # Enterprise User can only access products that they are a supplier for can [:create], Spree::Product - can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :override_variants, :clone, :destroy], Spree::Product do |product| + can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :variant_overrides, :clone, :destroy], Spree::Product do |product| OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier end - can :override_variants, nil + can :variant_overrides, nil can [:create], Spree::Variant can [:admin, :index, :read, :edit, :update, :search, :destroy], Spree::Variant do |variant| diff --git a/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface b/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface index 381f80a44d..88e720fd07 100644 --- a/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface +++ b/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface @@ -1,4 +1,4 @@ / insert_bottom "[data-hook='admin_product_sub_tabs']" -# Commented out until this feature is ready --#= tab :override_details, url: override_variants_admin_products_path, match_path: '/products/override_variants' +-#= tab :overrides, url: variant_overrides_admin_products_path, match_path: '/products/variant_overrides' diff --git a/app/views/spree/admin/products/override_variants.html.haml b/app/views/spree/admin/products/variant_overrides.html.haml similarity index 100% rename from app/views/spree/admin/products/override_variants.html.haml rename to app/views/spree/admin/products/variant_overrides.html.haml diff --git a/app/views/spree/admin/products/override_variants/_actions.html.haml b/app/views/spree/admin/products/variant_overrides/_actions.html.haml similarity index 100% rename from app/views/spree/admin/products/override_variants/_actions.html.haml rename to app/views/spree/admin/products/variant_overrides/_actions.html.haml diff --git a/app/views/spree/admin/products/override_variants/_data.html.haml b/app/views/spree/admin/products/variant_overrides/_data.html.haml similarity index 100% rename from app/views/spree/admin/products/override_variants/_data.html.haml rename to app/views/spree/admin/products/variant_overrides/_data.html.haml diff --git a/app/views/spree/admin/products/override_variants/_header.html.haml b/app/views/spree/admin/products/variant_overrides/_header.html.haml similarity index 100% rename from app/views/spree/admin/products/override_variants/_header.html.haml rename to app/views/spree/admin/products/variant_overrides/_header.html.haml diff --git a/app/views/spree/admin/products/override_variants/_hub_choice.html.haml b/app/views/spree/admin/products/variant_overrides/_hub_choice.html.haml similarity index 100% rename from app/views/spree/admin/products/override_variants/_hub_choice.html.haml rename to app/views/spree/admin/products/variant_overrides/_hub_choice.html.haml diff --git a/app/views/spree/admin/products/override_variants/_products.html.haml b/app/views/spree/admin/products/variant_overrides/_products.html.haml similarity index 100% rename from app/views/spree/admin/products/override_variants/_products.html.haml rename to app/views/spree/admin/products/variant_overrides/_products.html.haml diff --git a/app/views/spree/admin/products/override_variants/_products_product.html.haml b/app/views/spree/admin/products/variant_overrides/_products_product.html.haml similarity index 100% rename from app/views/spree/admin/products/override_variants/_products_product.html.haml rename to app/views/spree/admin/products/variant_overrides/_products_product.html.haml diff --git a/app/views/spree/admin/products/override_variants/_products_variants.html.haml b/app/views/spree/admin/products/variant_overrides/_products_variants.html.haml similarity index 100% rename from app/views/spree/admin/products/override_variants/_products_variants.html.haml rename to app/views/spree/admin/products/variant_overrides/_products_variants.html.haml diff --git a/config/routes.rb b/config/routes.rb index fd891fbb9c..9cea328411 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -118,7 +118,7 @@ Spree::Core::Engine.routes.prepend do match '/admin/reports/orders_and_fulfillment' => 'admin/reports#orders_and_fulfillment', :as => "orders_and_fulfillment_admin_reports", :via => [:get, :post] match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post] match '/admin/products/bulk_edit' => 'admin/products#bulk_edit', :as => "bulk_edit_admin_products" - match '/admin/products/override_variants' => 'admin/products#override_variants', :as => "override_variants_admin_products" + match '/admin/products/variant_overrides' => 'admin/products#variant_overrides', :as => "variant_overrides_admin_products" match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management" match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post] match '/admin/reports/customers' => 'admin/reports#customers', :as => "customers_admin_reports", :via => [:get, :post] diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/override_variants_spec.rb index 810cb1e0a9..0067683a8d 100644 --- a/spec/features/admin/override_variants_spec.rb +++ b/spec/features/admin/override_variants_spec.rb @@ -23,12 +23,12 @@ feature %q{ describe "selecting a hub" do it "displays a list of hub choices" do - visit '/admin/products/override_variants' + visit '/admin/products/variant_overrides' page.should have_select2 'hub_id', options: ['', hub.name, hub2.name] end it "displays the hub" do - visit '/admin/products/override_variants' + visit '/admin/products/variant_overrides' select2_select hub.name, from: 'hub_id' click_button 'Go' @@ -51,7 +51,7 @@ feature %q{ context "with no overrides" do before do - visit '/admin/products/override_variants' + visit '/admin/products/variant_overrides' select2_select hub.name, from: 'hub_id' click_button 'Go' end @@ -73,7 +73,7 @@ feature %q{ let!(:override) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111) } before do - visit '/admin/products/override_variants' + visit '/admin/products/variant_overrides' select2_select hub.name, from: 'hub_id' click_button 'Go' end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 1662ab7942..6c99038170 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -140,23 +140,23 @@ module Spree let(:order) {create(:order)} it "should be able to read/write their enterprises' products and variants" do - should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :override_variants, :clone, :destroy], for: p1) + should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :variant_overrides, :clone, :destroy], for: p1) should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p1.master) end it "should be able to read/write related enterprises' products and variants with manage_products permission" do er_p - should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :override_variants, :clone, :destroy], for: p_related) + should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :variant_overrides, :clone, :destroy], for: p_related) should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p_related.master) end it "should not be able to read/write other enterprises' products and variants" do - should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :override_variants, :clone, :destroy], for: p2) + should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :variant_overrides, :clone, :destroy], for: p2) should_not have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p2.master) end - it "should be able to override_variants on nil (required for override_variants)" do - should have_ability :override_variants, for: nil + it "should be able to variant_overrides on nil (required for variant_overrides)" do + should have_ability :variant_overrides, for: nil end it "should not be able to access admin actions on orders" do From d02511bf1d8d754b262f8dad559d97f5ff57458b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 11:55:08 +1100 Subject: [PATCH 356/681] WIP: Rename override_variants to variant_overrides - file and dir name changes --- ...face => add_variant_overrides_tab.html.haml.deface} | 0 .../spree/admin/products/variant_overrides.html.haml | 10 +++++----- .../products/variant_overrides/_products.html.haml | 4 ++-- ...ride_variants_spec.rb => variant_overrides_spec.rb} | 0 4 files changed, 7 insertions(+), 7 deletions(-) rename app/overrides/spree/admin/shared/_product_sub_menu/{add_override_variants_tab.html.haml.deface => add_variant_overrides_tab.html.haml.deface} (100%) rename spec/features/admin/{override_variants_spec.rb => variant_overrides_spec.rb} (100%) diff --git a/app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface b/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface similarity index 100% rename from app/overrides/spree/admin/shared/_product_sub_menu/add_override_variants_tab.html.haml.deface rename to app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface diff --git a/app/views/spree/admin/products/variant_overrides.html.haml b/app/views/spree/admin/products/variant_overrides.html.haml index 4c4cc46b4f..d4a0600712 100644 --- a/app/views/spree/admin/products/variant_overrides.html.haml +++ b/app/views/spree/admin/products/variant_overrides.html.haml @@ -1,11 +1,11 @@ -= render 'spree/admin/products/override_variants/header' -= render 'spree/admin/products/override_variants/data' += render 'spree/admin/products/variant_overrides/header' += render 'spree/admin/products/variant_overrides/data' %div{ ng: { app: 'ofn.admin', controller: 'AdminOverrideVariantsCtrl', init: 'initialise()' } } - = render 'spree/admin/products/override_variants/hub_choice' + = render 'spree/admin/products/variant_overrides/hub_choice' %div{ng: {show: 'hub'}} %h2 {{ hub.name }} - = render 'spree/admin/products/override_variants/actions' + = render 'spree/admin/products/variant_overrides/actions' - = render 'spree/admin/products/override_variants/products' + = render 'spree/admin/products/variant_overrides/products' diff --git a/app/views/spree/admin/products/variant_overrides/_products.html.haml b/app/views/spree/admin/products/variant_overrides/_products.html.haml index 3be701d203..fa6f3fdb32 100644 --- a/app/views/spree/admin/products/variant_overrides/_products.html.haml +++ b/app/views/spree/admin/products/variant_overrides/_products.html.haml @@ -6,6 +6,6 @@ %th Price %th On hand %tbody{ng: {repeat: 'product in products | hubPermissions:hubPermissions:hub.id'}} - = render 'spree/admin/products/override_variants/products_product' - = render 'spree/admin/products/override_variants/products_variants' + = render 'spree/admin/products/variant_overrides/products_product' + = render 'spree/admin/products/variant_overrides/products_variants' diff --git a/spec/features/admin/override_variants_spec.rb b/spec/features/admin/variant_overrides_spec.rb similarity index 100% rename from spec/features/admin/override_variants_spec.rb rename to spec/features/admin/variant_overrides_spec.rb From a6f116aa091310c36791ff1fea2aacd11f32ffba Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 11:59:00 +1100 Subject: [PATCH 357/681] WIP: Rename override_variants to variant_overrides - JS --- .../admin/controllers/override_variants_controller.js.coffee | 2 +- app/views/spree/admin/products/variant_overrides.html.haml | 2 +- .../controllers/override_variants_controller_spec.js.coffee | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee index 1d6fe64a70..560bd975e7 100644 --- a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, $timeout, Indexer, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides, DirtyVariantOverrides) -> +angular.module("ofn.admin").controller "AdminVariantOverridesCtrl", ($scope, $timeout, Indexer, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides, DirtyVariantOverrides) -> $scope.hubs = hubs $scope.hub = null $scope.products = [] diff --git a/app/views/spree/admin/products/variant_overrides.html.haml b/app/views/spree/admin/products/variant_overrides.html.haml index d4a0600712..0297e6c985 100644 --- a/app/views/spree/admin/products/variant_overrides.html.haml +++ b/app/views/spree/admin/products/variant_overrides.html.haml @@ -1,7 +1,7 @@ = render 'spree/admin/products/variant_overrides/header' = render 'spree/admin/products/variant_overrides/data' -%div{ ng: { app: 'ofn.admin', controller: 'AdminOverrideVariantsCtrl', init: 'initialise()' } } +%div{ ng: { app: 'ofn.admin', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } } = render 'spree/admin/products/variant_overrides/hub_choice' %div{ng: {show: 'hub'}} diff --git a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee index 7ab4c177dc..410d75cf50 100644 --- a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee @@ -1,4 +1,4 @@ -describe "OverrideVariantsCtrl", -> +describe "VariantOverridesCtrl", -> ctrl = null scope = null hubs = [{id: 1, name: 'Hub'}] @@ -18,7 +18,7 @@ describe "OverrideVariantsCtrl", -> inject ($controller, Indexer, _VariantOverrides_) -> VariantOverrides = _VariantOverrides_ - ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions, VariantOverrides: _VariantOverrides_} + ctrl = $controller 'AdminVariantOverridesCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions, VariantOverrides: _VariantOverrides_} it "initialises the hub list and the chosen hub", -> expect(scope.hubs).toEqual hubs From 91434fe12aa725284f86ffa11bc3a93a04657195 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 11:59:46 +1100 Subject: [PATCH 358/681] Rename override_variants to variant_overrides - JS file names --- ...ontroller.js.coffee => variant_overrides_controller.js.coffee} | 0 ...spec.js.coffee => variant_overrides_controller_spec.js.coffee} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/admin/controllers/{override_variants_controller.js.coffee => variant_overrides_controller.js.coffee} (100%) rename spec/javascripts/unit/admin/controllers/{override_variants_controller_spec.js.coffee => variant_overrides_controller_spec.js.coffee} (100%) diff --git a/app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee b/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee similarity index 100% rename from app/assets/javascripts/admin/controllers/override_variants_controller.js.coffee rename to app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee diff --git a/spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee similarity index 100% rename from spec/javascripts/unit/admin/controllers/override_variants_controller_spec.js.coffee rename to spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee From 9ee44e9aefae5142c022a471b2fa6c75a6a5e0d9 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 12:19:59 +1100 Subject: [PATCH 359/681] WIP: Extract variant overrides to own controller - move views --- .../admin/products => admin}/variant_overrides/_actions.html.haml | 0 .../admin/products => admin}/variant_overrides/_data.html.haml | 0 .../admin/products => admin}/variant_overrides/_header.html.haml | 0 .../products => admin}/variant_overrides/_hub_choice.html.haml | 0 .../products => admin}/variant_overrides/_products.html.haml | 0 .../variant_overrides/_products_product.html.haml | 0 .../variant_overrides/_products_variants.html.haml | 0 .../variant_overrides/index.html.haml} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename app/views/{spree/admin/products => admin}/variant_overrides/_actions.html.haml (100%) rename app/views/{spree/admin/products => admin}/variant_overrides/_data.html.haml (100%) rename app/views/{spree/admin/products => admin}/variant_overrides/_header.html.haml (100%) rename app/views/{spree/admin/products => admin}/variant_overrides/_hub_choice.html.haml (100%) rename app/views/{spree/admin/products => admin}/variant_overrides/_products.html.haml (100%) rename app/views/{spree/admin/products => admin}/variant_overrides/_products_product.html.haml (100%) rename app/views/{spree/admin/products => admin}/variant_overrides/_products_variants.html.haml (100%) rename app/views/{spree/admin/products/variant_overrides.html.haml => admin/variant_overrides/index.html.haml} (100%) diff --git a/app/views/spree/admin/products/variant_overrides/_actions.html.haml b/app/views/admin/variant_overrides/_actions.html.haml similarity index 100% rename from app/views/spree/admin/products/variant_overrides/_actions.html.haml rename to app/views/admin/variant_overrides/_actions.html.haml diff --git a/app/views/spree/admin/products/variant_overrides/_data.html.haml b/app/views/admin/variant_overrides/_data.html.haml similarity index 100% rename from app/views/spree/admin/products/variant_overrides/_data.html.haml rename to app/views/admin/variant_overrides/_data.html.haml diff --git a/app/views/spree/admin/products/variant_overrides/_header.html.haml b/app/views/admin/variant_overrides/_header.html.haml similarity index 100% rename from app/views/spree/admin/products/variant_overrides/_header.html.haml rename to app/views/admin/variant_overrides/_header.html.haml diff --git a/app/views/spree/admin/products/variant_overrides/_hub_choice.html.haml b/app/views/admin/variant_overrides/_hub_choice.html.haml similarity index 100% rename from app/views/spree/admin/products/variant_overrides/_hub_choice.html.haml rename to app/views/admin/variant_overrides/_hub_choice.html.haml diff --git a/app/views/spree/admin/products/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml similarity index 100% rename from app/views/spree/admin/products/variant_overrides/_products.html.haml rename to app/views/admin/variant_overrides/_products.html.haml diff --git a/app/views/spree/admin/products/variant_overrides/_products_product.html.haml b/app/views/admin/variant_overrides/_products_product.html.haml similarity index 100% rename from app/views/spree/admin/products/variant_overrides/_products_product.html.haml rename to app/views/admin/variant_overrides/_products_product.html.haml diff --git a/app/views/spree/admin/products/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml similarity index 100% rename from app/views/spree/admin/products/variant_overrides/_products_variants.html.haml rename to app/views/admin/variant_overrides/_products_variants.html.haml diff --git a/app/views/spree/admin/products/variant_overrides.html.haml b/app/views/admin/variant_overrides/index.html.haml similarity index 100% rename from app/views/spree/admin/products/variant_overrides.html.haml rename to app/views/admin/variant_overrides/index.html.haml From 531b35bbc6a8fbdb5594ebabce29f8082f3aa9fb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 12:25:57 +1100 Subject: [PATCH 360/681] WIP: Extract variant overrides to own controller - route, controller, views --- .../services/dirty_variant_overrides.js.coffee | 2 +- .../admin/variant_overrides_controller.rb | 17 +++++++++++++++++ .../admin/products_controller_decorator.rb | 8 -------- .../add_variant_overrides_tab.html.haml.deface | 2 +- .../admin/variant_overrides/_products.html.haml | 5 ++--- .../admin/variant_overrides/index.html.haml | 10 +++++----- config/routes.rb | 3 ++- spec/features/admin/variant_overrides_spec.rb | 8 ++++---- 8 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 app/controllers/admin/variant_overrides_controller.rb diff --git a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee index 72455326a2..82e7772982 100644 --- a/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/services/dirty_variant_overrides.js.coffee @@ -24,6 +24,6 @@ angular.module("ofn.admin").factory "DirtyVariantOverrides", ($http) -> save: -> $http method: "POST" - url: "/admin/products/variant_overrides" + url: "/admin/variant_overrides/bulk_update" data: variant_overrides: @all() diff --git a/app/controllers/admin/variant_overrides_controller.rb b/app/controllers/admin/variant_overrides_controller.rb new file mode 100644 index 0000000000..8d32e62b9d --- /dev/null +++ b/app/controllers/admin/variant_overrides_controller.rb @@ -0,0 +1,17 @@ +require 'open_food_network/spree_api_key_loader' + +module Admin + class VariantOverridesController < ResourceController + include OpenFoodNetwork::SpreeApiKeyLoader + include OrderCyclesHelper + before_filter :load_spree_api_key, only: :index + + def index + @hubs = order_cycle_hub_enterprises(without_validation: true) + @producers = order_cycle_producer_enterprises + @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). + order_cycle_enterprises_per_hub + @variant_overrides = VariantOverride.for_hubs(@hubs) + end + end +end diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index feb64ecf9b..c0e1f8b948 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -49,14 +49,6 @@ Spree::Admin::ProductsController.class_eval do end end - def variant_overrides - @hubs = order_cycle_hub_enterprises(without_validation: true) - @producers = order_cycle_producer_enterprises - @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). - order_cycle_enterprises_per_hub - @variant_overrides = VariantOverride.for_hubs(@hubs) - end - protected def location_after_save diff --git a/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface b/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface index 88e720fd07..1c61c06a0c 100644 --- a/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface +++ b/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface @@ -1,4 +1,4 @@ / insert_bottom "[data-hook='admin_product_sub_tabs']" -# Commented out until this feature is ready --#= tab :overrides, url: variant_overrides_admin_products_path, match_path: '/products/variant_overrides' +-#= tab :overrides, url: main_app.admin_variant_overrides_path, match_path: '/variant_overrides' diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index fa6f3fdb32..cf11e8ac5d 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -6,6 +6,5 @@ %th Price %th On hand %tbody{ng: {repeat: 'product in products | hubPermissions:hubPermissions:hub.id'}} - = render 'spree/admin/products/variant_overrides/products_product' - = render 'spree/admin/products/variant_overrides/products_variants' - + = render 'admin/variant_overrides/products_product' + = render 'admin/variant_overrides/products_variants' diff --git a/app/views/admin/variant_overrides/index.html.haml b/app/views/admin/variant_overrides/index.html.haml index 0297e6c985..8d7fc4e0b1 100644 --- a/app/views/admin/variant_overrides/index.html.haml +++ b/app/views/admin/variant_overrides/index.html.haml @@ -1,11 +1,11 @@ -= render 'spree/admin/products/variant_overrides/header' -= render 'spree/admin/products/variant_overrides/data' += render 'admin/variant_overrides/header' += render 'admin/variant_overrides/data' %div{ ng: { app: 'ofn.admin', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } } - = render 'spree/admin/products/variant_overrides/hub_choice' + = render 'admin/variant_overrides/hub_choice' %div{ng: {show: 'hub'}} %h2 {{ hub.name }} - = render 'spree/admin/products/variant_overrides/actions' + = render 'admin/variant_overrides/actions' - = render 'spree/admin/products/variant_overrides/products' + = render 'admin/variant_overrides/products' diff --git a/config/routes.rb b/config/routes.rb index 9cea328411..daccbbbe64 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -68,6 +68,8 @@ Openfoodnetwork::Application.routes.draw do get :move_up get :move_down end + + resources :variant_overrides end namespace :api do @@ -118,7 +120,6 @@ Spree::Core::Engine.routes.prepend do match '/admin/reports/orders_and_fulfillment' => 'admin/reports#orders_and_fulfillment', :as => "orders_and_fulfillment_admin_reports", :via => [:get, :post] match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post] match '/admin/products/bulk_edit' => 'admin/products#bulk_edit', :as => "bulk_edit_admin_products" - match '/admin/products/variant_overrides' => 'admin/products#variant_overrides', :as => "variant_overrides_admin_products" match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management" match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post] match '/admin/reports/customers' => 'admin/reports#customers', :as => "customers_admin_reports", :via => [:get, :post] diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 0067683a8d..c5ea4d6d0b 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -23,12 +23,12 @@ feature %q{ describe "selecting a hub" do it "displays a list of hub choices" do - visit '/admin/products/variant_overrides' + visit '/admin/variant_overrides' page.should have_select2 'hub_id', options: ['', hub.name, hub2.name] end it "displays the hub" do - visit '/admin/products/variant_overrides' + visit '/admin/variant_overrides' select2_select hub.name, from: 'hub_id' click_button 'Go' @@ -51,7 +51,7 @@ feature %q{ context "with no overrides" do before do - visit '/admin/products/variant_overrides' + visit '/admin/variant_overrides' select2_select hub.name, from: 'hub_id' click_button 'Go' end @@ -73,7 +73,7 @@ feature %q{ let!(:override) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111) } before do - visit '/admin/products/variant_overrides' + visit '/admin/variant_overrides' select2_select hub.name, from: 'hub_id' click_button 'Go' end From 27444c65893b6d453d2421b9848e828d63af9de9 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 10 Dec 2014 12:42:22 +1100 Subject: [PATCH 361/681] Extract variant overrides to own controller - permissions --- app/models/spree/ability_decorator.rb | 5 +++-- spec/models/spree/ability_spec.rb | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 97d4235b9a..aaabc53be8 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -65,16 +65,17 @@ class AbilityDecorator def add_product_management_abilities(user) # Enterprise User can only access products that they are a supplier for can [:create], Spree::Product - can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :variant_overrides, :clone, :destroy], Spree::Product do |product| + can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], Spree::Product do |product| OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier end - can :variant_overrides, nil can [:create], Spree::Variant can [:admin, :index, :read, :edit, :update, :search, :destroy], Spree::Variant do |variant| OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? variant.product.supplier end + can [:admin, :index], VariantOverride + can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty can [:admin, :index, :read, :create, :edit, :update, :destroy], Spree::Image diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 6c99038170..2962877b47 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -137,26 +137,26 @@ module Spree user end - let(:order) {create(:order)} + let(:order) { create(:order) } it "should be able to read/write their enterprises' products and variants" do - should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :variant_overrides, :clone, :destroy], for: p1) + should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p1) should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p1.master) end it "should be able to read/write related enterprises' products and variants with manage_products permission" do er_p - should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :variant_overrides, :clone, :destroy], for: p_related) + should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p_related) should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p_related.master) end it "should not be able to read/write other enterprises' products and variants" do - should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :variant_overrides, :clone, :destroy], for: p2) + should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p2) should_not have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p2.master) end - it "should be able to variant_overrides on nil (required for variant_overrides)" do - should have_ability :variant_overrides, for: nil + it "should be able to read/write variant overrides" do + should have_ability([:admin, :index], for: VariantOverride) end it "should not be able to access admin actions on orders" do From c38686c820958eecce83979b10289fc9539e0598 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Dec 2014 09:39:19 +1100 Subject: [PATCH 362/681] Create new variant overrides --- .../admin/services/status_message.js.coffee | 4 ++-- .../admin/variant_overrides_controller.rb | 23 +++++++++++++++++++ app/models/spree/ability_decorator.rb | 6 ++++- app/models/variant_override_set.rb | 5 ++++ config/routes.rb | 4 +++- spec/features/admin/variant_overrides_spec.rb | 17 ++++++++++++++ spec/models/spree/ability_spec.rb | 21 +++++++++++++---- 7 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 app/models/variant_override_set.rb diff --git a/app/assets/javascripts/admin/services/status_message.js.coffee b/app/assets/javascripts/admin/services/status_message.js.coffee index 914dabb390..aaa55cf339 100644 --- a/app/assets/javascripts/admin/services/status_message.js.coffee +++ b/app/assets/javascripts/admin/services/status_message.js.coffee @@ -2,9 +2,9 @@ angular.module("ofn.admin").factory "StatusMessage", ($timeout) -> new class StatusMessage types: progress: {timeout: false, style: {color: '#ff9906'}} - alert: {timeout: 3000, style: {color: 'grey'}} + alert: {timeout: 5000, style: {color: 'grey'}} notice: {timeout: false, style: {color: 'grey'}} - success: {timeout: 3000, style: {color: '#9fc820'}} + success: {timeout: 5000, style: {color: '#9fc820'}} failure: {timeout: false, style: {color: '#da5354'}} statusMessage: diff --git a/app/controllers/admin/variant_overrides_controller.rb b/app/controllers/admin/variant_overrides_controller.rb index 8d32e62b9d..5f2a2e205d 100644 --- a/app/controllers/admin/variant_overrides_controller.rb +++ b/app/controllers/admin/variant_overrides_controller.rb @@ -13,5 +13,28 @@ module Admin order_cycle_enterprises_per_hub @variant_overrides = VariantOverride.for_hubs(@hubs) end + + + def bulk_update + collection_hash = Hash[params[:variant_overrides].each_with_index.map { |vo, i| [i, vo] }] + vo_set = VariantOverrideSet.new collection_attributes: collection_hash + + # Ensure we're authorised to update all variant overrides + vo_set.collection.each { |vo| authorize! :update, vo } + + if vo_set.save + render json: {} + else + if vo_set.errors.present? + render json: { errors: vo_set.errors }, status: 400 + else + render nothing: true, status: 500 + end + end + end + + + def collection + end end end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index aaabc53be8..2c10e99b04 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -74,7 +74,11 @@ class AbilityDecorator OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? variant.product.supplier end - can [:admin, :index], VariantOverride + can [:admin, :index, :read, :update, :bulk_update], VariantOverride do |vo| + OpenFoodNetwork::Permissions.new(user). + order_cycle_enterprises.is_distributor. + include? vo.hub + end can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty can [:admin, :index, :read, :create, :edit, :update, :destroy], Spree::Image diff --git a/app/models/variant_override_set.rb b/app/models/variant_override_set.rb new file mode 100644 index 0000000000..cf36ee547b --- /dev/null +++ b/app/models/variant_override_set.rb @@ -0,0 +1,5 @@ +class VariantOverrideSet < ModelSet + def initialize(attributes={}) + super(VariantOverride, VariantOverride.all, nil, attributes) + end +end diff --git a/config/routes.rb b/config/routes.rb index daccbbbe64..23df433a0a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -69,7 +69,9 @@ Openfoodnetwork::Application.routes.draw do get :move_down end - resources :variant_overrides + resources :variant_overrides do + post :bulk_update, on: :collection + end end namespace :api do diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index c5ea4d6d0b..2775caf76c 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -67,6 +67,23 @@ feature %q{ page.should_not have_content producer2.name page.should_not have_content product2.name end + + it "creates new overrides" do + fill_in "variant-overrides-#{variant.id}-price", with: '777.77' + fill_in "variant-overrides-#{variant.id}-count-on-hand", with: '123' + page.should have_content "Changes to one override remain unsaved." + + expect do + click_button 'Save Changes' + page.should have_content "Changes saved." + end.to change(VariantOverride, :count).by(1) + + vo = VariantOverride.last + vo.variant_id.should == variant.id + vo.hub_id.should == hub.id + vo.price.should == 777.77 + vo.count_on_hand.should == 123 + end end context "with overrides" do diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 2962877b47..dee6f007de 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -155,10 +155,6 @@ module Spree should_not have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p2.master) end - it "should be able to read/write variant overrides" do - should have_ability([:admin, :index], for: VariantOverride) - end - it "should not be able to access admin actions on orders" do should_not have_ability([:admin], for: Spree::Order) end @@ -243,6 +239,23 @@ module Spree o end + let(:vo1) { create(:variant_override, hub: d1, variant: p1.master) } + let(:vo2) { create(:variant_override, hub: d2, variant: p2.master) } + + describe "variant overrides" do + it "should be able to access variant overrides page" do + should have_ability([:admin, :index, :bulk_update], for: VariantOverride) + end + + it "should be able to read/write their own variant overrides" do + should have_ability([:admin, :index, :read, :update], for: vo1) + end + + it "should not be able to read/write other enterprises' variant overrides" do + should_not have_ability([:admin, :index, :read, :update], for: vo2) + end + end + it "should be able to read/write their enterprises' orders" do should have_ability([:admin, :index, :read, :edit], for: o1) end From 0393007ba5d0a582b80f4e635f0b9c183036993b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Dec 2014 09:43:22 +1100 Subject: [PATCH 363/681] Update variant overrides --- spec/features/admin/variant_overrides_spec.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 2775caf76c..b1e112dfbc 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -87,7 +87,7 @@ feature %q{ end context "with overrides" do - let!(:override) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111) } + let!(:vo) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111) } before do visit '/admin/variant_overrides' @@ -99,6 +99,23 @@ feature %q{ page.should have_input "variant-overrides-#{variant.id}-price", with: '77.77', placeholder: '1.23' page.should have_input "variant-overrides-#{variant.id}-count-on-hand", with: '11111', placeholder: '12' end + + it "updates existing overrides" do + fill_in "variant-overrides-#{variant.id}-price", with: '22.22' + fill_in "variant-overrides-#{variant.id}-count-on-hand", with: '8888' + page.should have_content "Changes to one override remain unsaved." + + expect do + click_button 'Save Changes' + page.should have_content "Changes saved." + end.to change(VariantOverride, :count).by(0) + + vo.reload + vo.variant_id.should == variant.id + vo.hub_id.should == hub.id + vo.price.should == 22.22 + vo.count_on_hand.should == 8888 + end end end end From 45e709b2ccf9928b951f7925bafe20cecaf7c86e Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Dec 2014 10:39:26 +1100 Subject: [PATCH 364/681] VariantOverrides require hub and variant --- app/models/variant_override.rb | 4 +++- ...1210233407_add_not_null_to_variant_override_relations.rb | 6 ++++++ db/schema.rb | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20141210233407_add_not_null_to_variant_override_relations.rb diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index 4c6c29a5ec..f307125e88 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -1,6 +1,8 @@ class VariantOverride < ActiveRecord::Base - belongs_to :variant, class_name: 'Spree::Variant' belongs_to :hub, class_name: 'Enterprise' + belongs_to :variant, class_name: 'Spree::Variant' + + validates_presence_of :hub_id, :variant_id scope :for_hubs, lambda { |hubs| where(hub_id: hubs) diff --git a/db/migrate/20141210233407_add_not_null_to_variant_override_relations.rb b/db/migrate/20141210233407_add_not_null_to_variant_override_relations.rb new file mode 100644 index 0000000000..e10ab41953 --- /dev/null +++ b/db/migrate/20141210233407_add_not_null_to_variant_override_relations.rb @@ -0,0 +1,6 @@ +class AddNotNullToVariantOverrideRelations < ActiveRecord::Migration + def change + change_column :variant_overrides, :hub_id, :integer, null: false + change_column :variant_overrides, :variant_id, :integer, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 901705bbbc..3244916b3b 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 => 20141113053004) do +ActiveRecord::Schema.define(:version => 20141210233407) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -1034,8 +1034,8 @@ ActiveRecord::Schema.define(:version => 20141113053004) do end create_table "variant_overrides", :force => true do |t| - t.integer "variant_id" - t.integer "hub_id" + t.integer "variant_id", :null => false + t.integer "hub_id", :null => false t.decimal "price", :precision => 8, :scale => 2 t.integer "count_on_hand" end From fb980981fb8068a1e5cb38c350ed45c412dc30be Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Dec 2014 11:15:00 +1100 Subject: [PATCH 365/681] Display variant override errors --- .../variant_overrides_controller.js.coffee | 18 +++++++++-- spec/features/admin/variant_overrides_spec.rb | 32 +++++++++++++++++-- ...ariant_overrides_controller_spec.js.coffee | 14 +++++++- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee index 560bd975e7..3ef79b4b37 100644 --- a/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee @@ -46,7 +46,21 @@ angular.module("ofn.admin").controller "AdminVariantOverridesCtrl", ($scope, $ti DirtyVariantOverrides.save() .success (data) -> DirtyVariantOverrides.clear() - #VariantOverrides.update data.variant_overrides $timeout -> StatusMessage.display 'success', 'Changes saved.' .error (data, status) -> - $timeout -> StatusMessage.display 'failure', 'Oh no!' + $timeout -> StatusMessage.display 'failure', $scope.updateError(data, status) + + + $scope.updateError = (data, status) -> + if status == 401 + "I couldn't get authorisation to save those changes, so they remain unsaved." + + else if status == 400 && data.errors? + errors = [] + for field, field_errors of data.errors + errors = errors.concat field_errors + errors = errors.join ', ' + "I had some trouble saving: #{errors}" + + else + "Oh no! I was unable to save your changes." diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index b1e112dfbc..98d2251a31 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -14,7 +14,7 @@ feature %q{ let!(:hub) { create(:distributor_enterprise) } let!(:hub2) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } - let!(:er) { create(:enterprise_relationship, parent: producer, child: hub, + let!(:er1) { create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:add_to_order_cycle]) } context "as an enterprise user" do @@ -41,7 +41,7 @@ feature %q{ let!(:variant) { create(:variant, product: product, unit_value: 1, price: 1.23, on_hand: 12) } let!(:producer2) { create(:supplier_enterprise) } let!(:product2) { create(:simple_product, supplier: producer2) } - let!(:er) { create(:enterprise_relationship, parent: producer2, child: hub2, + let!(:er2) { create(:enterprise_relationship, parent: producer2, child: hub2, permissions_list: [:add_to_order_cycle]) } before do @@ -84,6 +84,34 @@ feature %q{ vo.price.should == 777.77 vo.count_on_hand.should == 123 end + + it "displays an error when unauthorised to access the page" do + fill_in "variant-overrides-#{variant.id}-price", with: '777.77' + fill_in "variant-overrides-#{variant.id}-count-on-hand", with: '123' + page.should have_content "Changes to one override remain unsaved." + + user.enterprises.clear + + expect do + click_button 'Save Changes' + page.should have_content "I couldn't get authorisation to save those changes, so they remain unsaved." + end.to change(VariantOverride, :count).by(0) + end + + it "displays an error when unauthorised to update a particular override" do + fill_in "variant-overrides-#{variant.id}-price", with: '777.77' + fill_in "variant-overrides-#{variant.id}-count-on-hand", with: '123' + page.should have_content "Changes to one override remain unsaved." + + EnterpriseRole.where(user_id: user).where('enterprise_id != ?', producer).destroy_all + er1.destroy + er2.destroy + + expect do + click_button 'Save Changes' + page.should have_content "I couldn't get authorisation to save those changes, so they remain unsaved." + end.to change(VariantOverride, :count).by(0) + end end context "with overrides" do diff --git a/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee index 410d75cf50..bdb62e8d37 100644 --- a/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee @@ -42,4 +42,16 @@ describe "VariantOverridesCtrl", -> it "does nothing when no selection has been made", -> scope.hub_id = '' scope.selectHub - expect(scope.hub).toBeNull \ No newline at end of file + expect(scope.hub).toBeNull + + describe "updating", -> + describe "error messages", -> + it "returns an unauthorised message upon 401", -> + expect(scope.updateError({}, 401)).toEqual "I couldn't get authorisation to save those changes, so they remain unsaved." + + it "returns errors when they are provided", -> + data = {errors: {base: ["Hub can't be blank", "Variant can't be blank"]}} + expect(scope.updateError(data, 400)).toEqual "I had some trouble saving: Hub can't be blank, Variant can't be blank" + + it "returns a generic message otherwise", -> + expect(scope.updateError({}, 500)).toEqual "Oh no! I was unable to save your changes." \ No newline at end of file From d83ff4ef3340a0c01204b7badaa561c4afbc0b29 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Dec 2014 11:16:07 +1100 Subject: [PATCH 366/681] Remove short wait, show variant overrides tab in admin --- .../add_variant_overrides_tab.html.haml.deface | 3 +-- spec/features/admin/variant_overrides_spec.rb | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface b/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface index 1c61c06a0c..0f4dde1798 100644 --- a/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface +++ b/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface @@ -1,4 +1,3 @@ / insert_bottom "[data-hook='admin_product_sub_tabs']" --# Commented out until this feature is ready --#= tab :overrides, url: main_app.admin_variant_overrides_path, match_path: '/variant_overrides' += tab :overrides, url: main_app.admin_variant_overrides_path, match_path: '/variant_overrides' diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 98d2251a31..3cacbd7490 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -9,8 +9,6 @@ feature %q{ include AuthenticationWorkflow include WebHelper - use_short_wait - let!(:hub) { create(:distributor_enterprise) } let!(:hub2) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } From d177f3ff73a1f1003709f88f5eb61bcb0d8ddcd1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Dec 2014 11:34:41 +1100 Subject: [PATCH 367/681] Variant overrides table styling --- app/views/admin/variant_overrides/_products_product.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/variant_overrides/_products_product.html.haml b/app/views/admin/variant_overrides/_products_product.html.haml index ccb3b97f6a..52551e8e0b 100644 --- a/app/views/admin/variant_overrides/_products_product.html.haml +++ b/app/views/admin/variant_overrides/_products_product.html.haml @@ -1,4 +1,4 @@ -%tr.product +%tr.product.even %td {{ producers[product.producer_id].name }} %td {{ product.name }} %td From ca1c116a5d0ea3546d03a33d814c9a0b7da8c3f1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Dec 2014 15:30:38 +1100 Subject: [PATCH 368/681] Swap param order --- app/models/enterprise_fee_set.rb | 4 ++-- app/models/enterprise_set.rb | 2 +- app/models/model_set.rb | 2 +- app/models/order_cycle_set.rb | 2 +- app/models/spree/product_set.rb | 2 +- app/models/variant_override_set.rb | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/models/enterprise_fee_set.rb b/app/models/enterprise_fee_set.rb index fdefeec277..e7cf857936 100644 --- a/app/models/enterprise_fee_set.rb +++ b/app/models/enterprise_fee_set.rb @@ -1,7 +1,7 @@ class EnterpriseFeeSet < ModelSet def initialize(attributes={}) super(EnterpriseFee, EnterpriseFee.all, - proc { |attrs| attrs[:name].blank? }, - attributes) + attributes, + proc { |attrs| attrs[:name].blank? }) end end diff --git a/app/models/enterprise_set.rb b/app/models/enterprise_set.rb index 0eef0ea3c8..123a6b5eed 100644 --- a/app/models/enterprise_set.rb +++ b/app/models/enterprise_set.rb @@ -1,5 +1,5 @@ class EnterpriseSet < ModelSet def initialize(collection, attributes={}) - super(Enterprise, collection, nil, attributes) + super(Enterprise, collection, attributes) end end diff --git a/app/models/model_set.rb b/app/models/model_set.rb index 24e84008a0..774b94beb8 100644 --- a/app/models/model_set.rb +++ b/app/models/model_set.rb @@ -5,7 +5,7 @@ class ModelSet attr_accessor :collection - def initialize(klass, collection, reject_if=nil, attributes={}) + def initialize(klass, collection, attributes={}, reject_if=nil) @klass, @collection, @reject_if = klass, collection, reject_if # Set here first, to ensure that we apply collection_attributes to the right collection diff --git a/app/models/order_cycle_set.rb b/app/models/order_cycle_set.rb index f83f40f029..d76eb8ef80 100644 --- a/app/models/order_cycle_set.rb +++ b/app/models/order_cycle_set.rb @@ -1,5 +1,5 @@ class OrderCycleSet < ModelSet def initialize(attributes={}) - super(OrderCycle, OrderCycle.all, nil, attributes) + super(OrderCycle, OrderCycle.all, attributes) end end diff --git a/app/models/spree/product_set.rb b/app/models/spree/product_set.rb index d6f361c82e..9b4b771f37 100644 --- a/app/models/spree/product_set.rb +++ b/app/models/spree/product_set.rb @@ -1,6 +1,6 @@ class Spree::ProductSet < ModelSet def initialize(attributes={}) - super(Spree::Product, [], proc { |attrs| attrs[:product_id].blank? }, attributes) + super(Spree::Product, [], attributes, proc { |attrs| attrs[:product_id].blank? }) end # A separate method of updating products was required due to an issue with the way Rails' assign_attributes and updates_attributes behave when delegated attributes of a nested diff --git a/app/models/variant_override_set.rb b/app/models/variant_override_set.rb index cf36ee547b..bdd0301dcb 100644 --- a/app/models/variant_override_set.rb +++ b/app/models/variant_override_set.rb @@ -1,5 +1,5 @@ class VariantOverrideSet < ModelSet def initialize(attributes={}) - super(VariantOverride, VariantOverride.all, nil, attributes) + super(VariantOverride, VariantOverride.all, attributes) end end From 31823f2dbdcb241f94d0f0bbdd08e94b5032c492 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Dec 2014 11:44:35 +1100 Subject: [PATCH 369/681] Setting both values to blank deletes override --- app/models/model_set.rb | 15 ++++- app/models/variant_override_set.rb | 3 +- spec/features/admin/variant_overrides_spec.rb | 13 +++++ spec/models/model_set_spec.rb | 58 +++++++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 spec/models/model_set_spec.rb diff --git a/app/models/model_set.rb b/app/models/model_set.rb index 774b94beb8..c9e1456f1a 100644 --- a/app/models/model_set.rb +++ b/app/models/model_set.rb @@ -5,8 +5,8 @@ class ModelSet attr_accessor :collection - def initialize(klass, collection, attributes={}, reject_if=nil) - @klass, @collection, @reject_if = klass, collection, reject_if + def initialize(klass, collection, attributes={}, reject_if=nil, delete_if=nil) + @klass, @collection, @reject_if, @delete_if = klass, collection, reject_if, delete_if # Set here first, to ensure that we apply collection_attributes to the right collection @collection = attributes[:collection] if attributes[:collection] @@ -36,7 +36,16 @@ class ModelSet end def save - collection.all?(&:save) + collection_to_delete.each &:destroy + collection_to_keep.all? &:save + end + + def collection_to_delete + collection.select { |e| @delete_if.andand.call(e.attributes) } + end + + def collection_to_keep + collection.reject { |e| @delete_if.andand.call(e.attributes) } end def persisted? diff --git a/app/models/variant_override_set.rb b/app/models/variant_override_set.rb index bdd0301dcb..fc02173862 100644 --- a/app/models/variant_override_set.rb +++ b/app/models/variant_override_set.rb @@ -1,5 +1,6 @@ class VariantOverrideSet < ModelSet def initialize(attributes={}) - super(VariantOverride, VariantOverride.all, attributes) + super(VariantOverride, VariantOverride.all, attributes, nil, + proc { |attrs| attrs['price'].blank? && attrs['count_on_hand'].blank? } ) end end diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 3cacbd7490..3ca116a5a7 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -142,6 +142,19 @@ feature %q{ vo.price.should == 22.22 vo.count_on_hand.should == 8888 end + + it "deletes overrides when values are cleared" do + fill_in "variant-overrides-#{variant.id}-price", with: '' + fill_in "variant-overrides-#{variant.id}-count-on-hand", with: '' + page.should have_content "Changes to one override remain unsaved." + + expect do + click_button 'Save Changes' + page.should have_content "Changes saved." + end.to change(VariantOverride, :count).by(-1) + + VariantOverride.where(id: vo.id).should be_empty + end end end end diff --git a/spec/models/model_set_spec.rb b/spec/models/model_set_spec.rb new file mode 100644 index 0000000000..24f452412f --- /dev/null +++ b/spec/models/model_set_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe ModelSet do + describe "updating" do + it "creates new models" do + attrs = {collection_attributes: {'1' => {name: 'e1', description: 'foo'}, + '2' => {name: 'e2', description: 'bar'}}} + + ms = ModelSet.new(EnterpriseGroup, EnterpriseGroup.all, attrs) + + expect { ms.save }.to change(EnterpriseGroup, :count).by(2) + + EnterpriseGroup.where(name: ['e1', 'e2']).count.should == 2 + end + + + it "updates existing models" do + e1 = create(:enterprise_group) + e2 = create(:enterprise_group) + + attrs = {collection_attributes: {'1' => {id: e1.id, name: 'e1zz', description: 'foo'}, + '2' => {id: e2.id, name: 'e2yy', description: 'bar'}}} + + ms = ModelSet.new(EnterpriseGroup, EnterpriseGroup.all, attrs) + + expect { ms.save }.to change(EnterpriseGroup, :count).by(0) + + EnterpriseGroup.where(name: ['e1zz', 'e2yy']).count.should == 2 + end + + + it "destroys deleted models" do + e1 = create(:enterprise) + e2 = create(:enterprise) + + attrs = {collection_attributes: {'1' => {id: e1.id, name: 'deleteme'}, + '2' => {id: e2.id, name: 'e2'}}} + + ms = ModelSet.new(Enterprise, Enterprise.all, attrs, nil, + proc { |attrs| attrs['name'] == 'deleteme' }) + + expect { ms.save }.to change(Enterprise, :count).by(-1) + + Enterprise.where(id: e1.id).should be_empty + Enterprise.where(id: e2.id).should be_present + end + + + it "ignores deletable new records" do + attrs = {collection_attributes: {'1' => {name: 'deleteme'}}} + + ms = ModelSet.new(Enterprise, Enterprise.all, attrs, nil, + proc { |attrs| attrs['name'] == 'deleteme' }) + + expect { ms.save }.to change(Enterprise, :count).by(0) + end + end +end From 84b607433c7c7a50d7dd55f6fdb7aebf80bdebfb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Dec 2014 12:06:55 +1100 Subject: [PATCH 370/681] Creating and then updating the new override updates the same override instead of creating a duplicate --- .../variant_overrides_controller.js.coffee | 3 +- .../services/variant_overrides.js.coffee | 4 +++ .../admin/variant_overrides_controller.rb | 3 +- .../api/admin/variant_override_serializer.rb | 2 +- spec/features/admin/variant_overrides_spec.rb | 32 +++++++++++++++++++ .../services/variant_overrides_spec.js.coffee | 15 +++++++++ 6 files changed, 56 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee index 3ef79b4b37..81cc7d3317 100644 --- a/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee @@ -44,8 +44,9 @@ angular.module("ofn.admin").controller "AdminVariantOverridesCtrl", ($scope, $ti else StatusMessage.display 'progress', 'Saving...' DirtyVariantOverrides.save() - .success (data) -> + .success (updatedVos) -> DirtyVariantOverrides.clear() + VariantOverrides.updateIds updatedVos $timeout -> StatusMessage.display 'success', 'Changes saved.' .error (data, status) -> $timeout -> StatusMessage.display 'failure', $scope.updateError(data, status) diff --git a/app/assets/javascripts/admin/services/variant_overrides.js.coffee b/app/assets/javascripts/admin/services/variant_overrides.js.coffee index 69a55e9f35..28d65eab03 100644 --- a/app/assets/javascripts/admin/services/variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/services/variant_overrides.js.coffee @@ -17,3 +17,7 @@ angular.module("ofn.admin").factory "VariantOverrides", (variantOverrides, Index hub_id: hub.id price: '' count_on_hand: '' + + updateIds: (updatedVos) -> + for vo in updatedVos + @variantOverrides[vo.hub_id][vo.variant_id].id = vo.id \ No newline at end of file diff --git a/app/controllers/admin/variant_overrides_controller.rb b/app/controllers/admin/variant_overrides_controller.rb index 5f2a2e205d..e46d5b1a6b 100644 --- a/app/controllers/admin/variant_overrides_controller.rb +++ b/app/controllers/admin/variant_overrides_controller.rb @@ -23,7 +23,8 @@ module Admin vo_set.collection.each { |vo| authorize! :update, vo } if vo_set.save - render json: {} + # Return saved VOs with IDs + render json: vo_set.collection, each_serializer: Api::Admin::VariantOverrideSerializer else if vo_set.errors.present? render json: { errors: vo_set.errors }, status: 400 diff --git a/app/serializers/api/admin/variant_override_serializer.rb b/app/serializers/api/admin/variant_override_serializer.rb index bc057adc78..ebe76a1049 100644 --- a/app/serializers/api/admin/variant_override_serializer.rb +++ b/app/serializers/api/admin/variant_override_serializer.rb @@ -1,3 +1,3 @@ class Api::Admin::VariantOverrideSerializer < ActiveModel::Serializer - attributes :id, :variant_id, :hub_id, :price, :count_on_hand + attributes :id, :hub_id, :variant_id, :price, :count_on_hand end diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 3ca116a5a7..aa2338a219 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -83,6 +83,38 @@ feature %q{ vo.count_on_hand.should == 123 end + describe "creating and then updating the new override" do + it "updates the same override instead of creating a duplicate" do + # When I create a new override + fill_in "variant-overrides-#{variant.id}-price", with: '777.77' + fill_in "variant-overrides-#{variant.id}-count-on-hand", with: '123' + page.should have_content "Changes to one override remain unsaved." + + expect do + click_button 'Save Changes' + page.should have_content "Changes saved." + end.to change(VariantOverride, :count).by(1) + + # And I update its settings without reloading the page + fill_in "variant-overrides-#{variant.id}-price", with: '111.11' + fill_in "variant-overrides-#{variant.id}-count-on-hand", with: '111' + page.should have_content "Changes to one override remain unsaved." + + # Then I shouldn't see a new override + expect do + click_button 'Save Changes' + page.should have_content "Changes saved." + end.to change(VariantOverride, :count).by(0) + + # And the override should be updated + vo = VariantOverride.last + vo.variant_id.should == variant.id + vo.hub_id.should == hub.id + vo.price.should == 111.11 + vo.count_on_hand.should == 111 + end + end + it "displays an error when unauthorised to access the page" do fill_in "variant-overrides-#{variant.id}-price", with: '777.77' fill_in "variant-overrides-#{variant.id}-count-on-hand", with: '123' diff --git a/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee b/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee index 7bd0605de4..532bb1d65c 100644 --- a/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee @@ -51,3 +51,18 @@ describe "VariantOverrides service", -> 300: { hub_id: 30, variant_id: 300, price: '', count_on_hand: ''} 400: { hub_id: 30, variant_id: 400, price: '', count_on_hand: ''} 500: { hub_id: 30, variant_id: 500, price: '', count_on_hand: ''} + + it "updates the IDs of variant overrides", -> + VariantOverrides.variantOverrides[2] = {} + VariantOverrides.variantOverrides[2][3] = {hub_id: 2, variant_id: 3, price: "4.0", count_on_hand: 5} + VariantOverrides.variantOverrides[2][8] = {hub_id: 2, variant_id: 8, price: "9.0", count_on_hand: 10} + + updatedVos = [ + {id: 1, hub_id: 2, variant_id: 3, price: "4.0", count_on_hand: 5} + {id: 6, hub_id: 2, variant_id: 8, price: "9.0", count_on_hand: 10} + ] + + VariantOverrides.updateIds updatedVos + + expect(VariantOverrides.variantOverrides[2][3].id).toEqual 1 + expect(VariantOverrides.variantOverrides[2][8].id).toEqual 6 From 66669e66ab4c3fd9dd3b3449424c56507e4f471b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 12 Dec 2014 15:43:05 +1100 Subject: [PATCH 371/681] Shaving some seconds from BOM spec by using simple order cycles and splitting specs up into more logical contexts --- .../admin/bulk_order_management_spec.rb | 246 ++++++++++-------- 1 file changed, 143 insertions(+), 103 deletions(-) diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index faba967e47..c8ad230a00 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -168,119 +168,159 @@ feature %q{ end context "using drop down seletors" do - let!(:oc1) { FactoryGirl.create(:order_cycle) } - let!(:oc2) { FactoryGirl.create(:order_cycle) } - let!(:s1) { oc1.suppliers.first } - let!(:s2) { oc2.suppliers.last } - let!(:d1) { oc1.distributors.first } - let!(:d2) { oc2.distributors.last } - let!(:p1) { FactoryGirl.create(:product, supplier: s1) } - let!(:p2) { FactoryGirl.create(:product, supplier: s2) } - let!(:o1) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: d1, order_cycle: oc1 ) } - let!(:o2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: d2, order_cycle: oc2 ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1, product: p1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2, product: p2 ) } + context "supplier filter" do + let!(:s1) { create(:supplier_enterprise) } + let!(:s2) { create(:supplier_enterprise) } + let!(:o1) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now ) } + let!(:li1) { FactoryGirl.create(:line_item, order: o1, product: create(:product, supplier: s1) ) } + let!(:li2) { FactoryGirl.create(:line_item, order: o1, product: create(:product, supplier: s2) ) } - before :each do - visit '/admin/orders/bulk_management' + before :each do + visit '/admin/orders/bulk_management' + end + + it "displays a select box for producers, which filters line items by the selected supplier" do + supplier_names = ["All"] + Enterprise.is_primary_producer.each{ |e| supplier_names << e.name } + find("div.select2-container#s2id_supplier_filter").click + supplier_names.each { |sn| page.should have_selector "div.select2-drop-active ul.select2-results li", text: sn } + find("div.select2-container#s2id_supplier_filter").click + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should have_selector "tr#li_#{li2.id}", visible: true + select2_select s1.name, from: "supplier_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}" + end + + it "displays all line items when 'All' is selected from supplier filter" do + select2_select s1.name, from: "supplier_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}", visible: true + select2_select "All", from: "supplier_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should have_selector "tr#li_#{li2.id}", visible: true + end end - it "displays a select box for producers, which filters line items by the selected supplier" do - supplier_names = ["All"] - Enterprise.is_primary_producer.each{ |e| supplier_names << e.name } - find("div.select2-container#s2id_supplier_filter").click - supplier_names.each { |sn| page.should have_selector "div.select2-drop-active ul.select2-results li", text: sn } - find("div.select2-container#s2id_supplier_filter").click - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should have_selector "tr#li_#{li2.id}", visible: true - select2_select s1.name, from: "supplier_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}" + context "distributor filter" do + let!(:d1) { create(:distributor_enterprise) } + let!(:d2) { create(:distributor_enterprise) } + let!(:o1) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: d1 ) } + let!(:o2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: d2 ) } + let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } + let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } + + before :each do + visit '/admin/orders/bulk_management' + end + + it "displays a select box for distributors, which filters line items by the selected distributor" do + distributor_names = ["All"] + Enterprise.is_distributor.each{ |e| distributor_names << e.name } + find("div.select2-container#s2id_distributor_filter").click + distributor_names.each { |dn| page.should have_selector "div.select2-drop-active ul.select2-results li", text: dn } + find("div.select2-container#s2id_distributor_filter").click + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should have_selector "tr#li_#{li2.id}", visible: true + select2_select d1.name, from: "distributor_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}", visible: true + end + + it "displays all line items when 'All' is selected from distributor filter" do + select2_select d1.name, from: "distributor_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}", visible: true + select2_select "All", from: "distributor_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should have_selector "tr#li_#{li2.id}", visible: true + end end - it "displays all line items when 'All' is selected from supplier filter" do - select2_select s1.name, from: "supplier_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}", visible: true - select2_select "All", from: "supplier_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should have_selector "tr#li_#{li2.id}", visible: true + context "order_cycle filter" do + let!(:oc1) { FactoryGirl.create(:simple_order_cycle ) } + let!(:oc2) { FactoryGirl.create(:simple_order_cycle ) } + let!(:o1) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, order_cycle: oc1 ) } + let!(:o2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, order_cycle: oc2 ) } + let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } + let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } + + before :each do + visit '/admin/orders/bulk_management' + end + + it "displays a select box for order cycles, which filters line items by the selected order cycle" do + order_cycle_names = ["All"] + OrderCycle.all.each{ |oc| order_cycle_names << oc.name } + find("div.select2-container#s2id_order_cycle_filter").click + order_cycle_names.each { |ocn| page.should have_selector "div.select2-drop-active ul.select2-results li", text: ocn } + find("div.select2-container#s2id_order_cycle_filter").click + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should have_selector "tr#li_#{li2.id}", visible: true + select2_select oc1.name, from: "order_cycle_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}", visible: true + end + + it "displays all line items when 'All' is selected from order_cycle filter" do + select2_select oc1.name, from: "order_cycle_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}", visible: true + select2_select "All", from: "order_cycle_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should have_selector "tr#li_#{li2.id}", visible: true + end end - it "displays a select box for distributors, which filters line items by the selected distributor" do - distributor_names = ["All"] - Enterprise.is_distributor.each{ |e| distributor_names << e.name } - find("div.select2-container#s2id_distributor_filter").click - distributor_names.each { |dn| page.should have_selector "div.select2-drop-active ul.select2-results li", text: dn } - find("div.select2-container#s2id_distributor_filter").click - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should have_selector "tr#li_#{li2.id}", visible: true - select2_select d1.name, from: "distributor_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}", visible: true - end + context "combination of filters" do + let!(:s1) { create(:supplier_enterprise) } + let!(:s2) { create(:supplier_enterprise) } + let!(:d1) { create(:distributor_enterprise) } + let!(:d2) { create(:distributor_enterprise) } + let!(:oc1) { FactoryGirl.create(:simple_order_cycle, suppliers: [s1], distributors: [d1] ) } + let!(:oc2) { FactoryGirl.create(:simple_order_cycle, suppliers: [s2], distributors: [d2] ) } + let!(:p1) { FactoryGirl.create(:product, supplier: s1) } + let!(:p2) { FactoryGirl.create(:product, supplier: s2) } + let!(:o1) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: d1, order_cycle: oc1 ) } + let!(:o2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: d2, order_cycle: oc2 ) } + let!(:li1) { FactoryGirl.create(:line_item, order: o1, product: p1 ) } + let!(:li2) { FactoryGirl.create(:line_item, order: o2, product: p2 ) } - it "displays all line items when 'All' is selected from distributor filter" do - select2_select d1.name, from: "distributor_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}", visible: true - select2_select "All", from: "distributor_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should have_selector "tr#li_#{li2.id}", visible: true - end + before :each do + visit '/admin/orders/bulk_management' + end - it "displays a select box for order cycles, which filters line items by the selected order cycle" do - order_cycle_names = ["All"] - OrderCycle.all.each{ |oc| order_cycle_names << oc.name } - find("div.select2-container#s2id_order_cycle_filter").click - order_cycle_names.each { |ocn| page.should have_selector "div.select2-drop-active ul.select2-results li", text: ocn } - find("div.select2-container#s2id_order_cycle_filter").click - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should have_selector "tr#li_#{li2.id}", visible: true - select2_select oc1.name, from: "order_cycle_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}", visible: true - end + it "allows filters to be used in combination" do + select2_select oc1.name, from: "order_cycle_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}", visible: true + select2_select d1.name, from: "distributor_filter" + select2_select s1.name, from: "supplier_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}", visible: true + select2_select d2.name, from: "distributor_filter" + select2_select s2.name, from: "supplier_filter" + page.should_not have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}", visible: true + select2_select oc2.name, from: "order_cycle_filter" + page.should_not have_selector "tr#li_#{li1.id}", visible: true + page.should have_selector "tr#li_#{li2.id}", visible: true + end - it "displays all line items when 'All' is selected from order_cycle filter" do - select2_select oc1.name, from: "order_cycle_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}", visible: true - select2_select "All", from: "order_cycle_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should have_selector "tr#li_#{li2.id}", visible: true - end - - it "allows filters to be used in combination" do - select2_select oc1.name, from: "order_cycle_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}", visible: true - select2_select d1.name, from: "distributor_filter" - select2_select s1.name, from: "supplier_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}", visible: true - select2_select d2.name, from: "distributor_filter" - select2_select s2.name, from: "supplier_filter" - page.should_not have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}", visible: true - select2_select oc2.name, from: "order_cycle_filter" - page.should_not have_selector "tr#li_#{li1.id}", visible: true - page.should have_selector "tr#li_#{li2.id}", visible: true - end - - it "displays a 'Clear All' button which sets all select filters to 'All'" do - select2_select oc1.name, from: "order_cycle_filter" - select2_select d1.name, from: "distributor_filter" - select2_select s1.name, from: "supplier_filter" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should_not have_selector "tr#li_#{li2.id}", visible: true - page.should have_button "Clear All" - click_button "Clear All" - page.should have_selector "div#s2id_order_cycle_filter a.select2-choice", text: "All" - page.should have_selector "div#s2id_supplier_filter a.select2-choice", text: "All" - page.should have_selector "div#s2id_distributor_filter a.select2-choice", text: "All" - page.should have_selector "tr#li_#{li1.id}", visible: true - page.should have_selector "tr#li_#{li2.id}", visible: true + it "displays a 'Clear All' button which sets all select filters to 'All'" do + select2_select oc1.name, from: "order_cycle_filter" + select2_select d1.name, from: "distributor_filter" + select2_select s1.name, from: "supplier_filter" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should_not have_selector "tr#li_#{li2.id}", visible: true + page.should have_button "Clear All" + click_button "Clear All" + page.should have_selector "div#s2id_order_cycle_filter a.select2-choice", text: "All" + page.should have_selector "div#s2id_supplier_filter a.select2-choice", text: "All" + page.should have_selector "div#s2id_distributor_filter a.select2-choice", text: "All" + page.should have_selector "tr#li_#{li1.id}", visible: true + page.should have_selector "tr#li_#{li2.id}", visible: true + end end end From eeae72352b7c3f6568a7e51cdaec76923e13b2de Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Fri, 12 Dec 2014 10:11:33 +0000 Subject: [PATCH 372/681] Renamed methods and vars to better fit naming conventions --- app/helpers/spree/reports_helper.rb | 2 +- .../spree/admin/reports/order_cycle_management.html.haml | 5 +++-- lib/open_food_network/order_cycle_management_report.rb | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index 5860625991..22004577eb 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -12,7 +12,7 @@ module Spree orders.map { |o| o.payments.first.payment_method.andand.name }.uniq end - def report_distribution_options(orders) + def report_shipping_options(orders) orders.map { |o| o.shipping_method.andand.name }.uniq end diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml index 3428cf3977..4581d32bc6 100644 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -14,8 +14,9 @@ multiple: true, include_blank: true) %br %br - = select_tag(:distribution_name, - options_for_select(report_distribution_options(@orders), params[:distribution_name]), + = label_tag nil, "Shipping Method: " + = select_tag(:shipping_method_name, + options_for_select(report_shipping_options(@orders), params[:shipping_method_name]), include_blank: true) %br %br diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index 0ac838c027..bd7974b56e 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -32,7 +32,7 @@ module OpenFoodNetwork end def filter(orders) - filter_to_order_cycle filter_to_payment_method filter_to_distribution orders + filter_to_order_cycle filter_to_payment_method filter_to_shipping_method orders end def filter_to_payment_method (orders) @@ -43,9 +43,9 @@ module OpenFoodNetwork end end - def filter_to_distribution (orders) - if params[:distribution_name].present? - orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:distribution_name]) + def filter_to_shipping_method (orders) + if params[:shipping_method_name].present? + orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:shipping_method_name]) else orders end From 7e49bd634e443dfadbe954017342fddd33da6861 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Fri, 12 Dec 2014 18:23:43 +0000 Subject: [PATCH 373/681] Updated the specs with Rohans suggestions --- .../order_cycle_management_report.rb | 2 +- .../order_cycle_management_report_spec.rb | 46 ++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index bd7974b56e..f1529482ed 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -43,7 +43,7 @@ module OpenFoodNetwork end end - def filter_to_shipping_method (orders) + def filter_to_shipping_method (orders) if params[:shipping_method_name].present? orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:shipping_method_name]) else diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb index 74069f6849..c010d6f253 100644 --- a/spec/lib/open_food_network/order_cycle_management_report_spec.rb +++ b/spec/lib/open_food_network/order_cycle_management_report_spec.rb @@ -1,5 +1,7 @@ require 'spec_helper' +include AuthenticationWorkflow + module OpenFoodNetwork describe OrderCycleManagementReport do context "as a site admin" do @@ -26,12 +28,7 @@ module OpenFoodNetwork end context "as an enterprise user" do - let(:user) do - user = create(:user) - user.spree_roles = [] - user.save! - user - end + let!(:user) { create_enterprise_user } subject { OrderCycleManagementReport.new user } @@ -66,17 +63,22 @@ module OpenFoodNetwork end describe "filtering orders" do - let(:orders) { Spree::Order.scoped } - let(:supplier) { create(:supplier_enterprise) } + let!(:orders) { Spree::Order.scoped } + let!(:supplier) { create(:supplier_enterprise) } - it "returns all orders sans-params" do + let!(:oc1) { create(:simple_order_cycle) } + let!(:pm1) { create(:payment_method, name: "PM1") } + let!(:sm1) { create(:shipping_method, name: "ship1") } + let!(:order1) { create(:order, shipping_method: sm1, order_cycle: oc1) } + let!(:payment1) { create(:payment, order: order1, payment_method: pm1) } + + it "returns all orders sans-params" do subject.filter(orders).should == orders end it "filters to a specific order cycle" do - oc1 = create(:simple_order_cycle) + oc2 = create(:simple_order_cycle) - order1 = create(:order, order_cycle: oc1) order2 = create(:order, order_cycle: oc2) subject.stub(:params).and_return(order_cycle_id: oc1.id) @@ -84,39 +86,31 @@ module OpenFoodNetwork end it "filters to a payment method" do - pm1 = create(:payment_method, name: "PM1") - pm2 = create(:payment_method, name: "PM2") - order1 = create(:order) + + pm2 = create(:payment_method, name: "PM2") order2 = create(:order) - payment1 = create(:payment, :order => order1, :payment_method => pm1) - payment2 = create(:payment, :order => order2, :payment_method => pm2) + payment2 = create(:payment, order: order2, payment_method: pm2) subject.stub(:params).and_return(payment_method_name: pm1.name) subject.filter(orders).should == [order1] end it "filters to a shipping method" do - sm1 = create(:shipping_method, name: "ship1") + sm2 = create(:shipping_method, name: "ship2") - order1 = create(:order, shipping_method: sm1) order2 = create(:order, shipping_method: sm2) - subject.stub(:params).and_return(distribution_name: sm1.name) + subject.stub(:params).and_return(shipping_method_name: sm1.name) subject.filter(orders).should == [order1] end it "should do all the filters at once" do - pm1 = create(:payment_method, name: "PM1") - sm1 = create(:shipping_method, name: "ship1") - oc1 = create(:simple_order_cycle) - order1 = create(:order, order_cycle: oc1,shipping_method: sm1) - payment1 = create(:payment, :order => order1, :payment_method => pm1) subject.stub(:params).and_return( order_cycle_id: oc1.id, - distribution_name: sm1.name, + shipping_method_name: sm1.name, payment_method_name: pm1.name) - subject.filter(orders) + subject.filter(orders).should == [order1] end From 91c500417bd4a8bd4f638bff869b10b2fc661fd1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Dec 2014 14:34:55 +1100 Subject: [PATCH 374/681] Swap param order --- app/models/variant_override.rb | 2 +- lib/open_food_network/variant_proxy.rb | 2 +- spec/models/variant_override_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index f307125e88..dcb281ea80 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -8,7 +8,7 @@ class VariantOverride < ActiveRecord::Base where(hub_id: hubs) } - def self.price_for(variant, hub) + def self.price_for(hub, variant) VariantOverride.where(variant_id: variant, hub_id: hub).first.andand.price end end diff --git a/lib/open_food_network/variant_proxy.rb b/lib/open_food_network/variant_proxy.rb index 26e6796e9a..9863fccad2 100644 --- a/lib/open_food_network/variant_proxy.rb +++ b/lib/open_food_network/variant_proxy.rb @@ -12,7 +12,7 @@ module OpenFoodNetwork end def price - VariantOverride.price_for(@variant, @hub) || @variant.price + VariantOverride.price_for(@hub, @variant) || @variant.price end diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb index 082a7fb135..d592262c97 100644 --- a/spec/models/variant_override_spec.rb +++ b/spec/models/variant_override_spec.rb @@ -19,11 +19,11 @@ describe VariantOverride do it "returns the numeric price when present" do VariantOverride.create!(variant: variant, hub: hub, price: 12.34) - VariantOverride.price_for(variant, hub).should == 12.34 + VariantOverride.price_for(hub, variant).should == 12.34 end it "returns nil otherwise" do - VariantOverride.price_for(variant, hub).should be_nil + VariantOverride.price_for(hub, variant).should be_nil end end end From 6c300431d21d6ef2e060277c4531119861202289 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 17 Dec 2014 13:37:16 +1100 Subject: [PATCH 375/681] Move Product#variants_for to Variant.for_distribution scope --- app/models/spree/product_decorator.rb | 4 -- app/models/spree/variant_decorator.rb | 4 ++ app/serializers/api/product_serializer.rb | 4 +- app/views/shop/products.rabl | 51 ----------------------- spec/models/spree/product_spec.rb | 35 ---------------- spec/models/spree/variant_spec.rb | 35 ++++++++++++++++ 6 files changed, 42 insertions(+), 91 deletions(-) delete mode 100644 app/views/shop/products.rabl diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 5b3460a073..4ecdd55bfc 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -128,10 +128,6 @@ Spree::Product.class_eval do self.product_distributions.find_by_distributor_id(distributor) end - def variants_for(order_cycle, distributor) - self.variants.where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor)) - end - # overriding to check self.on_demand as well def has_stock? has_variants? ? variants.any?(&:in_stock?) : (on_demand || master.in_stock?) diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 29d2ed89c1..0993057450 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -39,6 +39,10 @@ Spree::Variant.class_eval do select('DISTINCT spree_variants.*') } + scope :for_distribution, lambda { |order_cycle, distributor| + where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor)) + } + def price_with_fees(distributor, order_cycle) price + fees_for(distributor, order_cycle) diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index ceeaf7148e..a3ba916d9e 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -41,6 +41,8 @@ class Api::CachedProductSerializer < ActiveModel::Serializer has_one :master, serializer: Api::VariantSerializer def variants - object.variants_for(options[:current_order_cycle], options[:current_distributor]).in_stock + object.variants. + for_distribution(options[:current_order_cycle], options[:current_distributor]). + in_stock end end diff --git a/app/views/shop/products.rabl b/app/views/shop/products.rabl deleted file mode 100644 index a337ec3b16..0000000000 --- a/app/views/shop/products.rabl +++ /dev/null @@ -1,51 +0,0 @@ -collection @products -attributes :id, :name, :permalink, :count_on_hand, :on_demand, :group_buy - -node do |product| - { - notes: strip_tags(product.notes), - description: strip_tags(product.description), - price: product.master.price_with_fees(current_distributor, current_order_cycle) - } -end - -child :supplier => :supplier do - attributes :id -end - -child :primary_taxon => :primary_taxon do - extends 'json/taxon' -end - -child :master => :master do - attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, :count_on_hand, :on_demand - child :images => :images do - attributes :id, :alt - node do |img| - {:small_url => img.attachment.url(:small, false), - :large_url => img.attachment.url(:large, false)} - end - end -end - -node :variants do |product| - product.variants_for(current_order_cycle, current_distributor).in_stock.map do |v| - {id: v.id, - is_master: v.is_master, - count_on_hand: v.count_on_hand, - name_to_display: v.name_to_display, - unit_to_display: v.unit_to_display, - on_demand: v.on_demand, - price: v.price_with_fees(current_distributor, current_order_cycle), - images: v.images.map { |i| {id: i.id, alt: i.alt, small_url: i.attachment.url(:small, false)} } - } - end -end - -child :taxons => :taxons do |taxon| - attributes :id -end - -child :properties => :properties do |property| - attributes :name, :presentation -end diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index f9a990830c..86d03dc404 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -379,41 +379,6 @@ module Spree end end - describe "finding variants for an order cycle and hub" do - let(:oc) { create(:simple_order_cycle) } - let(:s) { create(:supplier_enterprise) } - let(:d1) { create(:distributor_enterprise) } - let(:d2) { create(:distributor_enterprise) } - - let(:p1) { create(:simple_product) } - let(:p2) { create(:simple_product) } - let(:v1) { create(:variant, product: p1) } - let(:v2) { create(:variant, product: p2) } - - let(:p_external) { create(:simple_product) } - let(:v_external) { create(:variant, product: p_external) } - - let!(:ex_in) { create(:exchange, order_cycle: oc, sender: s, receiver: oc.coordinator, - incoming: true, variants: [v1, v2]) } - let!(:ex_out1) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d1, - incoming: false, variants: [v1]) } - let!(:ex_out2) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d2, - incoming: false, variants: [v2]) } - - it "returns variants in the order cycle and distributor" do - p1.variants_for(oc, d1).should == [v1] - p2.variants_for(oc, d2).should == [v2] - end - - it "does not return variants in the order cycle but not the distributor" do - p1.variants_for(oc, d2).should be_empty - p2.variants_for(oc, d1).should be_empty - end - - it "does not return variants not in the order cycle" do - p_external.variants_for(oc, d1).should be_empty - end - end describe "variant units" do context "when the product initially has no variant unit" do diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 7fb160ce3f..85deecc6ea 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -67,6 +67,41 @@ module Spree end end + describe "finding variants for an order cycle and hub" do + let(:oc) { create(:simple_order_cycle) } + let(:s) { create(:supplier_enterprise) } + let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } + + let(:p1) { create(:simple_product) } + let(:p2) { create(:simple_product) } + let(:v1) { create(:variant, product: p1) } + let(:v2) { create(:variant, product: p2) } + + let(:p_external) { create(:simple_product) } + let(:v_external) { create(:variant, product: p_external) } + + let!(:ex_in) { create(:exchange, order_cycle: oc, sender: s, receiver: oc.coordinator, + incoming: true, variants: [v1, v2]) } + let!(:ex_out1) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d1, + incoming: false, variants: [v1]) } + let!(:ex_out2) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d2, + incoming: false, variants: [v2]) } + + it "returns variants in the order cycle and distributor" do + p1.variants.for_distribution(oc, d1).should == [v1] + p2.variants.for_distribution(oc, d2).should == [v2] + end + + it "does not return variants in the order cycle but not the distributor" do + p1.variants.for_distribution(oc, d2).should be_empty + p2.variants.for_distribution(oc, d1).should be_empty + end + + it "does not return variants not in the order cycle" do + p_external.variants.for_distribution(oc, d1).should be_empty + end + end end describe "calculating the price with enterprise fees" do From 2ea7bdbec61efcde6c43e7db27e8194a43b9314f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 17 Dec 2014 14:01:59 +1100 Subject: [PATCH 376/681] Move controller spec to serializer spec --- spec/controllers/shop_controller_spec.rb | 8 -------- spec/serializers/product_serializer_spec.rb | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 spec/serializers/product_serializer_spec.rb diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index bfa68da912..8794b17932 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -111,14 +111,6 @@ describe ShopController do response.body.should be_empty end - it "scopes variants for a product to the order cycle and distributor" do - controller.stub(:current_order_cycle).and_return order_cycle - controller.stub(:current_distributor).and_return d - Spree::Product.any_instance.should_receive(:variants_for).with(order_cycle, d).and_return(m = double()) - m.stub(:in_stock).and_return [] - xhr :get, :products - end - context "RABL tests" do render_views before do diff --git a/spec/serializers/product_serializer_spec.rb b/spec/serializers/product_serializer_spec.rb new file mode 100644 index 0000000000..0091668dbb --- /dev/null +++ b/spec/serializers/product_serializer_spec.rb @@ -0,0 +1,14 @@ +describe Api::ProductSerializer do + let(:hub) { create(:distributor_enterprise) } + let(:oc) { create(:simple_order_cycle, distributors: [hub], variants: [v1]) } + let(:p) { create(:simple_product) } + let!(:v1) { create(:variant, product: p, unit_value: 3) } + let!(:v2) { create(:variant, product: p, unit_value: 5) } + + it "scopes variants to distribution" do + s = Api::ProductSerializer.new p, current_distributor: hub, current_order_cycle: oc + json = s.to_json + json.should include v1.options_text + json.should_not include v2.options_text + end +end From 23c7715929cd104d5b7c3c1abe243f68aacfe025 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 17 Dec 2014 14:02:30 +1100 Subject: [PATCH 377/681] Cosmetic change --- app/models/spree/variant_decorator.rb | 8 ++++---- spec/features/consumer/shopping/shopping_spec.rb | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 0993057450..5cd5c39155 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -9,12 +9,12 @@ Spree::Variant.class_eval do accepts_nested_attributes_for :images validates_presence_of :unit_value, - if: -> v { %w(weight volume).include? v.product.andand.variant_unit }, - unless: :is_master + if: -> v { %w(weight volume).include? v.product.andand.variant_unit }, + unless: :is_master validates_presence_of :unit_description, - if: -> v { v.product.andand.variant_unit.present? && v.unit_value.nil? }, - unless: :is_master + if: -> v { v.product.andand.variant_unit.present? && v.unit_value.nil? }, + unless: :is_master before_validation :update_weight_from_unit_value, if: -> v { v.product.present? } after_save :update_units diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index e70377cffc..a47b73ba6c 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -119,7 +119,6 @@ feature "As a consumer I want to shop with a distributor", js: true do 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_price "$43.00" end end From 0832a8f63d651b773705b2537580b1a97787d5f8 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 17 Dec 2014 14:15:08 +1100 Subject: [PATCH 378/681] Viewing products shows overridden prices --- app/serializers/api/product_serializer.rb | 5 ++- lib/open_food_network/product_proxy.rb | 2 + lib/open_food_network/variant_proxy.rb | 4 ++ .../shopping/variant_overrides_spec.rb | 38 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 spec/features/consumer/shopping/variant_overrides_spec.rb diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index a3ba916d9e..816d18c3ce 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -1,3 +1,5 @@ +require 'open_food_network/variant_proxy' + class Api::ProductSerializer < ActiveModel::Serializer # TODO # Prices can't be cached? How? @@ -43,6 +45,7 @@ class Api::CachedProductSerializer < ActiveModel::Serializer def variants object.variants. for_distribution(options[:current_order_cycle], options[:current_distributor]). - in_stock + in_stock. + map { |v| OpenFoodNetwork::VariantProxy.new(v, options[:current_distributor]) } end end diff --git a/lib/open_food_network/product_proxy.rb b/lib/open_food_network/product_proxy.rb index ecbce189a4..2d1cca0b72 100644 --- a/lib/open_food_network/product_proxy.rb +++ b/lib/open_food_network/product_proxy.rb @@ -1,3 +1,5 @@ +require 'open_food_network/variant_proxy' + module OpenFoodNetwork # Variants can have several fields overridden on a per-enterprise basis by the # VariantOverride model. These overrides can be applied to variants by wrapping their diff --git a/lib/open_food_network/variant_proxy.rb b/lib/open_food_network/variant_proxy.rb index 9863fccad2..7a16bb4114 100644 --- a/lib/open_food_network/variant_proxy.rb +++ b/lib/open_food_network/variant_proxy.rb @@ -15,6 +15,10 @@ module OpenFoodNetwork VariantOverride.price_for(@hub, @variant) || @variant.price end + def price_with_fees(distributor, order_cycle) + price + @variant.fees_for(distributor, order_cycle) + end + def method_missing(name, *args, &block) @variant.send(name, *args, &block) diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb new file mode 100644 index 0000000000..9f44fbcf83 --- /dev/null +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +feature "shopping with variant overrides defined", js: true do + include AuthenticationWorkflow + include WebHelper + include ShopWorkflow + include UIComponentHelper + + use_short_wait + + describe "viewing products" do + let(:hub) { create(:distributor_enterprise) } + let(:producer) { create(:supplier_enterprise) } + let(:oc) { create(:simple_order_cycle, suppliers: [producer], coordinator: hub, distributors: [hub]) } + let(:outgoing_exchange) { oc.exchanges.outgoing.first } + let(:product) { create(:simple_product, supplier: producer) } + let(:variant) { create(:variant, product: product, price: 11.11) } + let!(:vo) { create(:variant_override, hub: hub, variant: variant, price: 22.22) } + + before do + outgoing_exchange.variants << variant + visit shop_path + click_link hub.name + end + + it "shows the overridden price" do + page.should_not have_price "$11.11" + page.should have_price "$22.22" + end + + it "takes stock from the override" + it "calculates fees correctly" + it "shows the overridden price with fees in the quick cart" + it "shows the correct prices in the shopping cart" + it "shows the correct prices in the checkout" + it "creates the order with the correct prices" + end +end From e6eecd3ae25645e492022d4c9bb253200d6fd268 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 10:20:31 +1100 Subject: [PATCH 379/681] Replace proxies with modules --- app/serializers/api/product_serializer.rb | 10 ++++-- lib/open_food_network/product_proxy.rb | 23 ------------ lib/open_food_network/scope_variant_to_hub.rb | 18 ++++++++++ lib/open_food_network/variant_proxy.rb | 27 -------------- .../open_food_network/product_proxy_spec.rb | 27 -------------- .../scope_variant_to_hub_spec.rb | 35 +++++++++++++++++++ .../open_food_network/variant_proxy_spec.rb | 27 -------------- 7 files changed, 60 insertions(+), 107 deletions(-) delete mode 100644 lib/open_food_network/product_proxy.rb create mode 100644 lib/open_food_network/scope_variant_to_hub.rb delete mode 100644 lib/open_food_network/variant_proxy.rb delete mode 100644 spec/lib/open_food_network/product_proxy_spec.rb create mode 100644 spec/lib/open_food_network/scope_variant_to_hub_spec.rb delete mode 100644 spec/lib/open_food_network/variant_proxy_spec.rb diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index 816d18c3ce..9ca174d863 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -1,4 +1,4 @@ -require 'open_food_network/variant_proxy' +require 'open_food_network/scope_variant_to_hub' class Api::ProductSerializer < ActiveModel::Serializer # TODO @@ -43,9 +43,13 @@ class Api::CachedProductSerializer < ActiveModel::Serializer has_one :master, serializer: Api::VariantSerializer def variants + # We use the in_stock? method here instead of the in_stock scope because we need to + # look up the stock as overridden by VariantOverrides, and the scope method is not affected + # by them. + object.variants. for_distribution(options[:current_order_cycle], options[:current_distributor]). - in_stock. - map { |v| OpenFoodNetwork::VariantProxy.new(v, options[:current_distributor]) } + each { |v| v.scope_to_hub options[:current_distributor] }. + select(&:in_stock?) end end diff --git a/lib/open_food_network/product_proxy.rb b/lib/open_food_network/product_proxy.rb deleted file mode 100644 index 2d1cca0b72..0000000000 --- a/lib/open_food_network/product_proxy.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'open_food_network/variant_proxy' - -module OpenFoodNetwork - # Variants can have several fields overridden on a per-enterprise basis by the - # VariantOverride model. These overrides can be applied to variants by wrapping their - # products in this proxy, which wraps the product's variants in VariantProxy. - class ProductProxy - instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ } - - def initialize(product, hub) - @product = product - @hub = hub - end - - def variants - @product.variants.map { |v| VariantProxy.new(v, @hub) } - end - - def method_missing(name, *args, &block) - @product.send(name, *args, &block) - end - end -end diff --git a/lib/open_food_network/scope_variant_to_hub.rb b/lib/open_food_network/scope_variant_to_hub.rb new file mode 100644 index 0000000000..89a527ea19 --- /dev/null +++ b/lib/open_food_network/scope_variant_to_hub.rb @@ -0,0 +1,18 @@ +module OpenFoodNetwork + module ScopeVariantToHub + def price + VariantOverride.price_for(@hub, self) || super + end + + def count_on_hand + VariantOverride.count_on_hand_for(@hub, self) || super + end + end +end + +Spree::Variant.class_eval do + def scope_to_hub(hub) + extend OpenFoodNetwork::ScopeVariantToHub + @hub = hub + end +end diff --git a/lib/open_food_network/variant_proxy.rb b/lib/open_food_network/variant_proxy.rb deleted file mode 100644 index 7a16bb4114..0000000000 --- a/lib/open_food_network/variant_proxy.rb +++ /dev/null @@ -1,27 +0,0 @@ -module OpenFoodNetwork - # Variants can have several fields overridden on a per-enterprise basis by the - # VariantOverride model. These overrides can be applied to variants by wrapping in an - # instance of the VariantProxy class. This class proxies most methods back to the wrapped - # variant, but checks for overrides when fetching some properties. - class VariantProxy - instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ } - - def initialize(variant, hub) - @variant = variant - @hub = hub - end - - def price - VariantOverride.price_for(@hub, @variant) || @variant.price - end - - def price_with_fees(distributor, order_cycle) - price + @variant.fees_for(distributor, order_cycle) - end - - - def method_missing(name, *args, &block) - @variant.send(name, *args, &block) - end - end -end diff --git a/spec/lib/open_food_network/product_proxy_spec.rb b/spec/lib/open_food_network/product_proxy_spec.rb deleted file mode 100644 index e8a8565949..0000000000 --- a/spec/lib/open_food_network/product_proxy_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'open_food_network/product_proxy' -require 'open_food_network/variant_proxy' - -module OpenFoodNetwork - describe ProductProxy do - let(:hub) { double(:hub) } - let(:p) { double(:product, name: 'name') } - let(:pp) { ProductProxy.new(p, hub) } - - describe "delegating calls to proxied product" do - it "delegates name" do - pp.name.should == 'name' - end - end - - describe "fetching the variants" do - let(:v1) { double(:variant) } - let(:v2) { double(:variant) } - let(:p) { double(:product, variants: [v1, v2]) } - - it "returns variants wrapped in VariantProxy" do - # #class is proxied too, so we test that it worked by #object_id - pp.variants.map(&:object_id).sort.should_not == [v1.object_id, v2.object_id].sort - end - end - end -end diff --git a/spec/lib/open_food_network/scope_variant_to_hub_spec.rb b/spec/lib/open_food_network/scope_variant_to_hub_spec.rb new file mode 100644 index 0000000000..b657afeb9b --- /dev/null +++ b/spec/lib/open_food_network/scope_variant_to_hub_spec.rb @@ -0,0 +1,35 @@ +require 'open_food_network/scope_variant_to_hub' + +module OpenFoodNetwork + describe ScopeVariantToHub do + let(:hub) { create(:distributor_enterprise) } + let(:v) { create(:variant, price: 11.11, count_on_hand: 1) } + let(:vo) { create(:variant_override, hub: hub, variant: v, price: 22.22, count_on_hand: 2) } + + describe "overriding price" do + it "returns the overridden price when one is present" do + vo + v.scope_to_hub hub + v.price.should == 22.22 + end + + it "returns the variant's price otherwise" do + v.scope_to_hub hub + v.price.should == 11.11 + end + end + + describe "overriding stock levels" do + it "returns the overridden stock level when one is present" do + vo + v.scope_to_hub hub + v.count_on_hand.should == 2 + end + + it "returns the variant's stock level otherwise" do + v.scope_to_hub hub + v.count_on_hand.should == 1 + end + end + end +end diff --git a/spec/lib/open_food_network/variant_proxy_spec.rb b/spec/lib/open_food_network/variant_proxy_spec.rb deleted file mode 100644 index 6439c502a5..0000000000 --- a/spec/lib/open_food_network/variant_proxy_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'open_food_network/variant_proxy' - -module OpenFoodNetwork - describe VariantProxy do - let(:hub) { double(:hub) } - let(:v) { double(:variant, sku: 'sku123', price: 'global price') } - let(:vp) { VariantProxy.new(v, hub) } - - describe "delegating calls to proxied variant" do - it "delegates sku" do - vp.sku.should == 'sku123' - end - end - - describe "looking up the price" do - it "returns the override price when there is one" do - VariantOverride.stub(:price_for) { 'override price' } - vp.price.should == 'override price' - end - - it "returns the global price otherwise" do - VariantOverride.stub(:price_for) { nil } - vp.price.should == 'global price' - end - end - end -end From f5ee9ba2f31055113c5563aeb768107e83512f12 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 10:21:07 +1100 Subject: [PATCH 380/681] VariantOverride looks up count_on_hand --- app/models/variant_override.rb | 14 +++++++++++++- spec/models/variant_override_spec.rb | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index dcb281ea80..9549d02b2e 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -9,6 +9,18 @@ class VariantOverride < ActiveRecord::Base } def self.price_for(hub, variant) - VariantOverride.where(variant_id: variant, hub_id: hub).first.andand.price + self.for(hub, variant).andand.price end + + def self.count_on_hand_for(hub, variant) + self.for(hub, variant).andand.count_on_hand + end + + + private + + def self.for(hub, variant) + VariantOverride.where(variant_id: variant, hub_id: hub).first + end + end diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb index d592262c97..af6ecb91b7 100644 --- a/spec/models/variant_override_spec.rb +++ b/spec/models/variant_override_spec.rb @@ -26,4 +26,18 @@ describe VariantOverride do VariantOverride.price_for(hub, variant).should be_nil end end + + describe "looking up count on hand" do + let(:variant) { create(:variant) } + let(:hub) { create(:distributor_enterprise) } + + it "returns the numeric stock level when present" do + VariantOverride.create!(variant: variant, hub: hub, count_on_hand: 12) + VariantOverride.count_on_hand_for(hub, variant).should == 12 + end + + it "returns nil otherwise" do + VariantOverride.count_on_hand_for(hub, variant).should be_nil + end + end end From 1d3800696ee9a89be8298615beff9207732f680a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 10:40:56 +1100 Subject: [PATCH 381/681] Variant overrides can override stock levels --- app/controllers/shop_controller.rb | 13 ++++++--- app/models/spree/product_decorator.rb | 2 +- lib/open_food_network/scope_product_to_hub.rb | 16 +++++++++++ .../shopping/variant_overrides_spec.rb | 28 +++++++++++++++---- 4 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 lib/open_food_network/scope_product_to_hub.rb diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 2c1b4cd6d5..b229666524 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_product_to_hub' + class ShopController < BaseController layout "darkswarm" before_filter :require_distributor_chosen @@ -10,10 +12,13 @@ class ShopController < BaseController def products # Can we make this query less slow? # - if @products = current_order_cycle.andand - .valid_products_distributed_by(current_distributor).andand - .select { |p| !p.deleted? && p.has_stock_for_distribution?(current_order_cycle, current_distributor) }.andand - .sort_by {|p| p.name } + if current_order_cycle + @products = current_order_cycle. + valid_products_distributed_by(current_distributor). + each { |p| p.scope_to_hub current_distributor }. + select { |p| !p.deleted? && p.has_stock_for_distribution?(current_order_cycle, current_distributor) }. + sort_by(&:name) + render status: 200, json: ActiveModel::ArraySerializer.new(@products, each_serializer: Api::ProductSerializer, current_order_cycle: current_order_cycle, current_distributor: current_distributor).to_json diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 4ecdd55bfc..cc8b213bde 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -137,7 +137,7 @@ Spree::Product.class_eval do # This product has stock for a distribution if it is available on-demand # or if one of its variants in the distribution is in stock (!has_variants? && on_demand) || - variants_distributed_by(order_cycle, distributor).any? { |v| v.in_stock? } + variants_distributed_by(order_cycle, distributor).any?(&:in_stock?) end def variants_distributed_by(order_cycle, distributor) diff --git a/lib/open_food_network/scope_product_to_hub.rb b/lib/open_food_network/scope_product_to_hub.rb new file mode 100644 index 0000000000..7d5e0aa067 --- /dev/null +++ b/lib/open_food_network/scope_product_to_hub.rb @@ -0,0 +1,16 @@ +require 'open_food_network/scope_variant_to_hub' + +module OpenFoodNetwork + module ScopeProductToHub + def variants_distributed_by(order_cycle, distributor) + super.each { |v| v.scope_to_hub @hub } + end + end +end + +Spree::Product.class_eval do + def scope_to_hub(hub) + extend OpenFoodNetwork::ScopeProductToHub + @hub = hub + end +end diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index 9f44fbcf83..f96eeddc61 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -13,26 +13,42 @@ feature "shopping with variant overrides defined", js: true do let(:producer) { create(:supplier_enterprise) } let(:oc) { create(:simple_order_cycle, suppliers: [producer], coordinator: hub, distributors: [hub]) } let(:outgoing_exchange) { oc.exchanges.outgoing.first } - let(:product) { create(:simple_product, supplier: producer) } - let(:variant) { create(:variant, product: product, price: 11.11) } - let!(:vo) { create(:variant_override, hub: hub, variant: variant, price: 22.22) } + let(:p1) { create(:simple_product, supplier: producer) } + let(:p2) { create(:simple_product, supplier: producer) } + let(:v1) { create(:variant, product: p1, price: 11.11, unit_value: 1) } + let(:v2) { create(:variant, product: p1, price: 22.22, unit_value: 2) } + let(:v3) { create(:variant, product: p2, price: 33.33, unit_value: 3) } + let!(:vo1) { create(:variant_override, hub: hub, variant: v1, price: 99.99) } + let!(:vo2) { create(:variant_override, hub: hub, variant: v2, count_on_hand: 0) } + let!(:vo3) { create(:variant_override, hub: hub, variant: v3, count_on_hand: 0) } before do - outgoing_exchange.variants << variant + outgoing_exchange.variants << v1 + outgoing_exchange.variants << v2 + outgoing_exchange.variants << v3 visit shop_path click_link hub.name end it "shows the overridden price" do page.should_not have_price "$11.11" - page.should have_price "$22.22" + page.should have_price "$99.99" + end + + it "looks up stock from the override" do + # Product should appear but one of the variants is out of stock + page.should_not have_content v2.options_text + + # Entire product should not appear - no stock + page.should_not have_content p2.name + page.should_not have_content v3.options_text end - it "takes stock from the override" it "calculates fees correctly" it "shows the overridden price with fees in the quick cart" it "shows the correct prices in the shopping cart" it "shows the correct prices in the checkout" it "creates the order with the correct prices" + it "subtracts stock from the override" end end From ead84aa9ff0ff41a8d843331eaeab648272172c2 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 11:00:45 +1100 Subject: [PATCH 382/681] Add ids to product listing products and variants --- app/views/shop/products/_form.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index ef44a133eb..d86527a37f 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -17,11 +17,11 @@ %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", - "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | orderBy:ordering.order) track by product.id "} + "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | orderBy:ordering.order) track by product.id ", "id" => "product-{{ product.id }}"} = render partial: "shop/products/summary" - %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants"} - %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id"} + %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} + %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"} %product{"ng-show" => "Products.loading"} .row.summary From 5fcb2982fa32e2db4fd39d915166078e1687a850 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 11:53:17 +1100 Subject: [PATCH 383/681] Fees are calculated correctly for items with variant overrides --- .../templates/price_breakdown.html.haml | 12 ++++++------ .../consumer/shopping/variant_overrides_spec.rb | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index cf82e31457..21104b71a0 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -12,22 +12,22 @@ %li.cost .right {{ variant.price | localizeCurrency }} Item cost - %li{"bo-if" => "variant.fees.admin"} + %li.admin-fee{"bo-if" => "variant.fees.admin"} .right {{ variant.fees.admin | localizeCurrency }} Admin fee - %li{"bo-if" => "variant.fees.sales"} + %li.sales-fee{"bo-if" => "variant.fees.sales"} .right {{ variant.fees.sales | localizeCurrency }} Sales fee - %li{"bo-if" => "variant.fees.packing"} + %li.packing-fee{"bo-if" => "variant.fees.packing"} .right {{ variant.fees.packing | localizeCurrency }} Packing fee - %li{"bo-if" => "variant.fees.transport"} + %li.transport-fee{"bo-if" => "variant.fees.transport"} .right {{ variant.fees.transport | localizeCurrency }} Transport fee - %li{"bo-if" => "variant.fees.fundraising"} + %li.fundraising-fee{"bo-if" => "variant.fees.fundraising"} .right {{ variant.fees.fundraising | localizeCurrency }} Fundraising fee - %li + %li.total %strong .right = {{ variant.price_with_fees | localizeCurrency }}   diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index f96eeddc61..cda3b0f8ea 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -18,21 +18,23 @@ feature "shopping with variant overrides defined", js: true do let(:v1) { create(:variant, product: p1, price: 11.11, unit_value: 1) } let(:v2) { create(:variant, product: p1, price: 22.22, unit_value: 2) } let(:v3) { create(:variant, product: p2, price: 33.33, unit_value: 3) } - let!(:vo1) { create(:variant_override, hub: hub, variant: v1, price: 99.99) } + let!(:vo1) { create(:variant_override, hub: hub, variant: v1, price: 55.55) } let!(:vo2) { create(:variant_override, hub: hub, variant: v2, count_on_hand: 0) } let!(:vo3) { create(:variant_override, hub: hub, variant: v3, count_on_hand: 0) } + let(:ef) { create(:enterprise_fee, enterprise: hub, fee_type: 'packing', calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } before do outgoing_exchange.variants << v1 outgoing_exchange.variants << v2 outgoing_exchange.variants << v3 + outgoing_exchange.enterprise_fees << ef visit shop_path click_link hub.name end it "shows the overridden price" do page.should_not have_price "$11.11" - page.should have_price "$99.99" + page.should have_price "$61.11" end it "looks up stock from the override" do @@ -44,7 +46,14 @@ feature "shopping with variant overrides defined", js: true do page.should_not have_content v3.options_text end - it "calculates fees correctly" + it "calculates fees correctly" do + page.find("#variant-#{v1.id} .graph-button").click + page.find(".price_breakdown a").click + page.should have_selector 'li.cost div', text: '$55.55' + page.should have_selector 'li.packing-fee div', text: '$5.56' + page.should have_selector 'li.total div', text: '= $61.11' + end + it "shows the overridden price with fees in the quick cart" it "shows the correct prices in the shopping cart" it "shows the correct prices in the checkout" From b0f5d0170c51b5dc359c1876cc11994d96b609cc Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 13:32:28 +1100 Subject: [PATCH 384/681] Spec that overridden price with fees appears in quick cart --- app/views/shared/menu/_cart.html.haml | 8 ++++---- .../features/consumer/shopping/variant_overrides_spec.rb | 9 ++++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index 345365735b..74f9ee2b29 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -12,7 +12,7 @@ %h5 Your shopping cart %ul %li.product-cart{"ng-repeat" => "line_item in Cart.line_items_present()", - "ng-controller" => "LineItemCtrl"} + "ng-controller" => "LineItemCtrl", "id" => "cart-variant-{{ line_item.variant.id }}"} .row .columns.small-7 %small @@ -20,15 +20,15 @@ %em {{ line_item.variant.unit_to_display }} .columns.small-3.text-right %small - {{line_item.quantity}} + %span.quantity {{ line_item.quantity }} %i.ofn-i_009-close - {{ line_item.variant.price_with_fees | localizeCurrency }} + %span.price {{ line_item.variant.price_with_fees | localizeCurrency }} .columns.small-2 %small \= %strong - .right {{ line_item.variant.totalPrice() | localizeCurrency }} + .total-price.right {{ line_item.variant.totalPrice() | localizeCurrency }} %li.total-cart{"ng-show" => "Cart.line_items_present().length > 0"} .row diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index cda3b0f8ea..727bc5700e 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -54,7 +54,14 @@ feature "shopping with variant overrides defined", js: true do page.should have_selector 'li.total div', text: '= $61.11' end - it "shows the overridden price with fees in the quick cart" + it "shows the overridden price with fees in the quick cart" do + fill_in "variants[#{v1.id}]", with: "2" + show_cart + page.should have_selector "#cart-variant-#{v1.id} .quantity", text: '2' + page.should have_selector "#cart-variant-#{v1.id} .price", text: '61.11' + page.should have_selector "#cart-variant-#{v1.id} .total-price", text: '122.22' + end + it "shows the correct prices in the shopping cart" it "shows the correct prices in the checkout" it "creates the order with the correct prices" From c4b45bdbbf4bf869500c6ef7113a99c90ffabe8b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 15:28:02 +1100 Subject: [PATCH 385/681] Fix line items returning prices with fractional cents --- app/models/spree/line_item_decorator.rb | 8 ++++---- spec/models/spree/line_item_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index 3333ed8735..28be16d1a4 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -26,7 +26,7 @@ Spree::LineItem.class_eval do def price_with_adjustments # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, # so line_item.adjustments returns an empty array - price + order.adjustments.where(source_id: id).sum(&:amount) / quantity + (price + order.adjustments.where(source_id: id).sum(&:amount) / quantity).round(2) end def single_display_amount_with_adjustments @@ -34,9 +34,9 @@ Spree::LineItem.class_eval do end def amount_with_adjustments - # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, - # so line_item.adjustments returns an empty array - amount + order.adjustments.where(source_id: id).sum(&:amount) + # We calculate from price_with_adjustments here rather than building our own value because + # rounding errors can produce discrepencies of $0.01. + price_with_adjustments * quantity end def display_amount_with_adjustments diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb index ef25d139ac..6166732b76 100644 --- a/spec/models/spree/line_item_spec.rb +++ b/spec/models/spree/line_item_spec.rb @@ -25,5 +25,27 @@ module Spree LineItem.supplied_by_any([s1, s2]).sort.should == [li1, li2].sort end end + + describe "calculating price with adjustments" do + it "does not return fractional cents" do + li = LineItem.new + + li.stub(:price) { 55.55 } + li.stub_chain(:order, :adjustments, :where, :sum) { 11.11 } + li.stub(:quantity) { 2 } + li.price_with_adjustments.should == 61.11 + end + end + + describe "calculating amount with adjustments" do + it "returns a value consistent with price_with_adjustments" do + li = LineItem.new + + li.stub(:price) { 55.55 } + li.stub_chain(:order, :adjustments, :where, :sum) { 11.11 } + li.stub(:quantity) { 2 } + li.amount_with_adjustments.should == 122.22 + end + end end end From 3b58d99abc3f0d8fb4d587d75f320cc07add6c70 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 16:38:03 +1100 Subject: [PATCH 386/681] Fix spec failure --- spec/features/admin/reports_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 62fdb9c565..298086c2e4 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -70,7 +70,7 @@ feature %q{ rows = find("table#listing_order_payment_methods").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } table.sort.should == [ - ["First Name", "Last Name", "Email", "Phone", "Hub", "Payment Method", "Amount", "Amount Paid"] + ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount", "Amount Paid"] ].sort end end From 343af1f1e07ac901cbd6dc79cffe6d2eea06769c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 16:54:13 +1100 Subject: [PATCH 387/681] Rename report_shipping_options to report_shipping_method_options for symmetry with report_payment_method_options --- app/helpers/spree/reports_helper.rb | 2 +- app/views/spree/admin/reports/order_cycle_management.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index 22004577eb..6a1e206a24 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -12,7 +12,7 @@ module Spree orders.map { |o| o.payments.first.payment_method.andand.name }.uniq end - def report_shipping_options(orders) + def report_shipping_method_options(orders) orders.map { |o| o.shipping_method.andand.name }.uniq end diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml index 4581d32bc6..5c8db4a2f9 100644 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -16,7 +16,7 @@ %br = label_tag nil, "Shipping Method: " = select_tag(:shipping_method_name, - options_for_select(report_shipping_options(@orders), params[:shipping_method_name]), + options_for_select(report_shipping_method_options(@orders), params[:shipping_method_name]), include_blank: true) %br %br From d5485156848981696004df37ccd7a6bbc63ff377 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 17:03:57 +1100 Subject: [PATCH 388/681] Remove unused header column --- lib/open_food_network/order_cycle_management_report.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index f1529482ed..ae5f9bf7fa 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -7,7 +7,7 @@ module OpenFoodNetwork end def header - ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount ", "Amount Paid"] + ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount"] end From 0151ecbb3212640d8321adbae4305c7c868928f1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 17:04:10 +1100 Subject: [PATCH 389/681] Add additional rows to payment method selection --- app/views/spree/admin/reports/order_cycle_management.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml index 5c8db4a2f9..aa59e5a046 100644 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -11,7 +11,7 @@ = select_tag(:payment_method_name, options_for_select(report_payment_method_options(@orders), params[:payment_method_name]), - multiple: true, include_blank: true) + multiple: true, include_blank: true, size: 10) %br %br = label_tag nil, "Shipping Method: " From 27d646c0e807dc81dd565d44a975586735138eaa Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 17:07:18 +1100 Subject: [PATCH 390/681] Tweak indentation, remove some blank lines --- .../order_cycle_management_report_spec.rb | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb index c010d6f253..c68daa3b61 100644 --- a/spec/lib/open_food_network/order_cycle_management_report_spec.rb +++ b/spec/lib/open_food_network/order_cycle_management_report_spec.rb @@ -5,7 +5,7 @@ include AuthenticationWorkflow module OpenFoodNetwork describe OrderCycleManagementReport do context "as a site admin" do - let(:user) do + let(:user) do user = create(:user) user.spree_roles << Spree::Role.find_or_create_by_name!("admin") user @@ -28,7 +28,7 @@ module OpenFoodNetwork end context "as an enterprise user" do - let!(:user) { create_enterprise_user } + let!(:user) { create_enterprise_user } subject { OrderCycleManagementReport.new user } @@ -71,13 +71,12 @@ module OpenFoodNetwork let!(:sm1) { create(:shipping_method, name: "ship1") } let!(:order1) { create(:order, shipping_method: sm1, order_cycle: oc1) } let!(:payment1) { create(:payment, order: order1, payment_method: pm1) } - + it "returns all orders sans-params" do subject.filter(orders).should == orders end it "filters to a specific order cycle" do - oc2 = create(:simple_order_cycle) order2 = create(:order, order_cycle: oc2) @@ -86,7 +85,6 @@ module OpenFoodNetwork end it "filters to a payment method" do - pm2 = create(:payment_method, name: "PM2") order2 = create(:order) payment2 = create(:payment, order: order2, payment_method: pm2) @@ -96,7 +94,6 @@ module OpenFoodNetwork end it "filters to a shipping method" do - sm2 = create(:shipping_method, name: "ship2") order2 = create(:order, shipping_method: sm2) @@ -105,16 +102,11 @@ module OpenFoodNetwork end it "should do all the filters at once" do - - subject.stub(:params).and_return( - order_cycle_id: oc1.id, - shipping_method_name: sm1.name, - payment_method_name: pm1.name) + subject.stub(:params).and_return(order_cycle_id: oc1.id, + shipping_method_name: sm1.name, + payment_method_name: pm1.name) subject.filter(orders).should == [order1] - - end - - + end end end end From 6e0576235d034ce1ed89716dc39f0af5cee4807d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 17:17:15 +1100 Subject: [PATCH 391/681] Use create\! instead of build - more concise, raises exception on error --- .../open_food_network/order_cycle_management_report_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb index c68daa3b61..bdf6ad3bca 100644 --- a/spec/lib/open_food_network/order_cycle_management_report_spec.rb +++ b/spec/lib/open_food_network/order_cycle_management_report_spec.rb @@ -39,9 +39,9 @@ module OpenFoodNetwork it "only shows orders managed by the current user" do d1 = create(:distributor_enterprise) - d1.enterprise_roles.build(user: user).save + d1.enterprise_roles.create!(user: user) d2 = create(:distributor_enterprise) - d2.enterprise_roles.build(user: create(:user)).save + d2.enterprise_roles.create!(user: create(:user)) o1 = create(:order, distributor: d1, completed_at: 1.day.ago) o2 = create(:order, distributor: d2, completed_at: 1.day.ago) @@ -53,7 +53,7 @@ module OpenFoodNetwork it "does not show orders through a hub that the current user does not manage" do # Given a supplier enterprise with an order for one of its products - supplier.enterprise_roles.build(user: user).save + supplier.enterprise_roles.create!(user: user) order.line_items << create(:line_item, product: product) # When I fetch orders, I should see no orders From e9f04c3c15cde465d6574edcdfa110f9860b501c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 18 Dec 2014 17:17:24 +1100 Subject: [PATCH 392/681] Clean up whitespace --- .../admin/reports_controller_decorator.rb | 2 +- app/helpers/spree/reports_helper.rb | 4 ++-- ...der_cycle_management_description.html.haml | 2 +- .../reports/order_cycle_management.html.haml | 8 +++---- config/routes.rb | 2 +- .../order_cycle_management_report.rb | 24 ++++++++----------- .../order_cycle_management_report_spec.rb | 12 +++++----- spec/models/spree/order_spec.rb | 6 ++--- 8 files changed, 28 insertions(+), 32 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index eab6d7d162..6da368eff1 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -68,7 +68,7 @@ Spree::Admin::ReportsController.class_eval do @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) @orders = @search.result - + render_report(@report.header, @report.table, params[:csv], "customers.csv") end diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index 6a1e206a24..81310b6c62 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -13,8 +13,8 @@ module Spree end def report_shipping_method_options(orders) - orders.map { |o| o.shipping_method.andand.name }.uniq - end + orders.map { |o| o.shipping_method.andand.name }.uniq + end end end diff --git a/app/views/spree/admin/reports/_order_cycle_management_description.html.haml b/app/views/spree/admin/reports/_order_cycle_management_description.html.haml index d4d20ae66e..0a1ee7f69c 100644 --- a/app/views/spree/admin/reports/_order_cycle_management_description.html.haml +++ b/app/views/spree/admin/reports/_order_cycle_management_description.html.haml @@ -1,4 +1,4 @@ %ul{style: "margin-left: 12pt"} - report_types.each do |report_type| - %li + %li = link_to report_type[0], "#{order_cycle_management_admin_reports_url}?report_type=#{report_type[1]}" diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml index aa59e5a046..fbe365c742 100644 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -9,15 +9,15 @@ = label_tag nil, "Payment Methods (hold Ctrl to select multiple payment methods)" %br - = select_tag(:payment_method_name, - options_for_select(report_payment_method_options(@orders), params[:payment_method_name]), + = select_tag(:payment_method_name, + options_for_select(report_payment_method_options(@orders), params[:payment_method_name]), multiple: true, include_blank: true, size: 10) %br %br = label_tag nil, "Shipping Method: " - = select_tag(:shipping_method_name, + = select_tag(:shipping_method_name, options_for_select(report_shipping_method_options(@orders), params[:shipping_method_name]), - include_blank: true) + include_blank: true) %br %br = check_box_tag :csv diff --git a/config/routes.rb b/config/routes.rb index 15025cf88c..37c1974875 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -116,7 +116,7 @@ end Spree::Core::Engine.routes.prepend do match '/admin/reports/orders_and_distributors' => 'admin/reports#orders_and_distributors', :as => "orders_and_distributors_admin_reports", :via => [:get, :post] - match '/admin/reports/order_cycle_management' => 'admin/reports#order_cycle_management', :as => "order_cycle_management_admin_reports", :via => [:get, :post] + match '/admin/reports/order_cycle_management' => 'admin/reports#order_cycle_management', :as => "order_cycle_management_admin_reports", :via => [:get, :post] match '/admin/reports/group_buys' => 'admin/reports#group_buys', :as => "group_buys_admin_reports", :via => [:get, :post] match '/admin/reports/bulk_coop' => 'admin/reports#bulk_coop', :as => "bulk_coop_admin_reports", :via => [:get, :post] match '/admin/reports/payments' => 'admin/reports#payments', :as => "payments_admin_reports", :via => [:get, :post] diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index ae5f9bf7fa..06a90bfe25 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -8,7 +8,6 @@ module OpenFoodNetwork def header ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount"] - end def table @@ -16,14 +15,14 @@ module OpenFoodNetwork ba = order.billing_address da = order.distributor.andand.address [ba.firstname, - ba.lastname, - order.email, - ba.phone, - order.distributor.andand.name, - order.shipping_method.andand.name, - order.payments.first.andand.payment_method.andand.name, - order.payments.first.amount - ] + ba.lastname, + order.email, + ba.phone, + order.distributor.andand.name, + order.shipping_method.andand.name, + order.payments.first.andand.payment_method.andand.name, + order.payments.first.amount + ] end end @@ -35,7 +34,7 @@ module OpenFoodNetwork filter_to_order_cycle filter_to_payment_method filter_to_shipping_method orders end - def filter_to_payment_method (orders) + def filter_to_payment_method(orders) if params[:payment_method_name].present? orders.with_payment_method_name(params[:payment_method_name]) else @@ -43,7 +42,7 @@ module OpenFoodNetwork end end - def filter_to_shipping_method (orders) + def filter_to_shipping_method(orders) if params[:shipping_method_name].present? orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:shipping_method_name]) else @@ -58,8 +57,5 @@ module OpenFoodNetwork orders end end - - end end - diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb index bdf6ad3bca..e0e7cb40bf 100644 --- a/spec/lib/open_food_network/order_cycle_management_report_spec.rb +++ b/spec/lib/open_food_network/order_cycle_management_report_spec.rb @@ -72,7 +72,7 @@ module OpenFoodNetwork let!(:order1) { create(:order, shipping_method: sm1, order_cycle: oc1) } let!(:payment1) { create(:payment, order: order1, payment_method: pm1) } - it "returns all orders sans-params" do + it "returns all orders sans-params" do subject.filter(orders).should == orders end @@ -84,16 +84,16 @@ module OpenFoodNetwork subject.filter(orders).should == [order1] end - it "filters to a payment method" do - pm2 = create(:payment_method, name: "PM2") - order2 = create(:order) - payment2 = create(:payment, order: order2, payment_method: pm2) + it "filters to a payment method" do + pm2 = create(:payment_method, name: "PM2") + order2 = create(:order) + payment2 = create(:payment, order: order2, payment_method: pm2) subject.stub(:params).and_return(payment_method_name: pm1.name) subject.filter(orders).should == [order1] end - it "filters to a shipping method" do + it "filters to a shipping method" do sm2 = create(:shipping_method, name: "ship2") order2 = create(:order, shipping_method: sm2) diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 28c4997032..114f774a1d 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -77,7 +77,7 @@ describe Spree::Order do subject.update_distribution_charge! end - it "ensures the correct adjustment(s) are created for order cycles" do + it "ensures the correct adjustment(s) are created for order cycles" do EnterpriseFee.stub(:clear_all_adjustments_on_order) line_item = double(:line_item) subject.stub(:line_items) { [line_item] } @@ -348,12 +348,12 @@ describe Spree::Order do it "returns the order with payment method name" do Spree::Order.with_payment_method_name('foo').should == [o1] end - + it "doesn't return rows with a different payment method name" do Spree::Order.with_payment_method_name('foobar').should_not include o1 Spree::Order.with_payment_method_name('foobar').should_not include o2 end - + it "doesn't return duplicate rows" do p2 = FactoryGirl.create(:payment, :order => o1, :payment_method => pm1) Spree::Order.with_payment_method_name('foo').length.should == 1 From 72118f4e2efcef07750ac793f7a75d9e968e02a5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 19 Dec 2014 10:01:46 +1100 Subject: [PATCH 393/681] Fix syntax error, spec for removed column --- app/controllers/spree/admin/reports_controller_decorator.rb | 2 +- spec/features/admin/reports_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 6da368eff1..b346c45b67 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -638,7 +638,7 @@ Spree::Admin::ReportsController.class_eval do :customers => {:name => "Customers", :description => 'Customer details'}, :products_and_inventory => {:name => "Products & Inventory", :description => ''}, :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" }, - :users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" } + :users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" }, :order_cycle_management => {:name => "Order Cycle Management", :description => ''} } # Return only reports the user is authorized to view. diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 298086c2e4..0524a6769e 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -70,7 +70,7 @@ feature %q{ rows = find("table#listing_order_payment_methods").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } table.sort.should == [ - ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount", "Amount Paid"] + ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount"] ].sort end end From 08d37b955a77b7cf25c65ec968604e6a33eaa8d5 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 12:22:04 +1100 Subject: [PATCH 394/681] Refactoring filter feature for shopfront page - show by default, hide on click. WIP. --- .../darkswarm/active_table_search.css.sass | 22 ++++++++++++++---- .../_filter_box_shopfront.html.haml | 4 ++++ .../_filter_controls_shopfront.html.haml | 8 +++++++ app/views/shop/products/_filters.html.haml | 23 +++++++++++-------- 4 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 app/views/shared/components/_filter_box_shopfront.html.haml create mode 100644 app/views/shared/components/_filter_controls_shopfront.html.haml diff --git a/app/assets/stylesheets/darkswarm/active_table_search.css.sass b/app/assets/stylesheets/darkswarm/active_table_search.css.sass index 0503e30d0f..614509784e 100644 --- a/app/assets/stylesheets/darkswarm/active_table_search.css.sass +++ b/app/assets/stylesheets/darkswarm/active_table_search.css.sass @@ -3,12 +3,12 @@ @import big-input @import animations -// OVERRIDES +// Filter-box .row .row.filter-box margin-left: 0 margin-right: 0 -.row.filter-box:first-child +.row.filter-box:first-child, .row.filter-box.filter-box-shopfront border: 1px solid $clr-blue-light @include border-radius(0.25em) margin-top: 2px @@ -17,17 +17,20 @@ background: transparent margin-top: 1em +.row.filter-box.filter-box-shopfront + margin-top: 0 + products .filter-box background: #f7f7f7 + .filter-box background: rgba(245,245,245,0.6) - .tdhead padding: 0.25rem 0.5rem margin-top: 0.9rem color: $clr-blue - border-bottom: 1px solid $clr-blue-light + border-bottom: 1px solid $clr-blue-light // OVERRIDES [class*="block-grid-"] @@ -89,7 +92,6 @@ products .filter-box svg path fill: $clr-brick - render-svg display: block @@ -103,6 +105,16 @@ products .filter-box path fill: #666 +.filter-box.filter-box-shopfront + .tdhead + margin-top: 0rem + margin-bottom: 0.75rem + padding: 0.5rem 0 + h5 + color: $clr-blue + .button.tiny + margin-bottom: 0rem + .button.filterbtn margin-bottom: 0 !important min-width: 160px diff --git a/app/views/shared/components/_filter_box_shopfront.html.haml b/app/views/shared/components/_filter_box_shopfront.html.haml new file mode 100644 index 0000000000..f73d13f69f --- /dev/null +++ b/app/views/shared/components/_filter_box_shopfront.html.haml @@ -0,0 +1,4 @@ +/ %span.animate-show{"ng-show" => "filtersActive && totalActive() > 0"} +%a.button.secondary.tiny{"ng-click" => "clearAll()"} + %i.ofn-i_009-close + Clear all filters diff --git a/app/views/shared/components/_filter_controls_shopfront.html.haml b/app/views/shared/components/_filter_controls_shopfront.html.haml new file mode 100644 index 0000000000..b89d55c452 --- /dev/null +++ b/app/views/shared/components/_filter_controls_shopfront.html.haml @@ -0,0 +1,8 @@ +%a.button.success.tiny.filterbtn{"ng-click" => "filtersActive = !filtersActive", +"ng-show" => "FilterSelectorsService.selectors.length > 0"} + {{ filterText(filtersActive) }} + %i.ofn-i_005-caret-down{"ng-show" => "!filtersActive"} + %i.ofn-i_006-caret-up{"ng-show" => "filtersActive"} + +%a.button.secondary.tiny.filterbtn.disabled{"ng-show" => "FilterSelectorsService.selectors.length == 0"} + No filters diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index c135274fad..a62361465c 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,17 +1,20 @@ -.row - = render partial: 'shared/components/filter_controls' - .small-12.medium-6.columns.text-right -   - .row.animate-show{"ng-show" => "filtersActive"} .small-12.columns - .row.filter-box - .small-12.columns - %h5.tdhead + = render partial: 'shared/components/filter_controls_shopfront' + +.row.filter-box.filter-box-shopfront.animate-hide{"ng-hide" => "filtersActive"} + .small-12.columns + .row.tdhead + .small-12.medium-6.columns + %h5 .light Filter by - Type + Category + .small-12.medium-6.columns.text-right + = render partial: 'shared/components/filter_controls_shopfront' + = render partial: 'shared/components/filter_box_shopfront' + .row + .small-12.columns %ul.small-block-grid-2.medium-block-grid-3.large-block-grid-4 %taxon-selector{objects: "Products.products | products:query | products:showProfiles", results: "activeTaxons"} -= render partial: 'shared/components/filter_box' From 515348a8fb33b8cb0ee93c24cf55b4a4b8a74ec6 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 12:52:57 +1100 Subject: [PATCH 395/681] Change the filtersActive variable for this page only --- .../darkswarm/controllers/products_controller.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index 8ed56f252d..f445cbac91 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -5,6 +5,7 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, $scope.clearAll = FilterSelectorsService.clearAll $scope.filterText = FilterSelectorsService.filterText $scope.FilterSelectorsService = FilterSelectorsService + $scope.filtersActive = true $scope.limit = 3 $scope.ordering = order: "primary_taxon.name" From ebe2d9929982339004cd91cf1dd91b0d26cba19e Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 12:53:26 +1100 Subject: [PATCH 396/681] Change markup because the boolean variable now works correctly --- app/views/shop/products/_filters.html.haml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/shop/products/_filters.html.haml b/app/views/shop/products/_filters.html.haml index a62361465c..395e6f123d 100644 --- a/app/views/shop/products/_filters.html.haml +++ b/app/views/shop/products/_filters.html.haml @@ -1,8 +1,8 @@ -.row.animate-show{"ng-show" => "filtersActive"} +.row.animate-show{"ng-hide" => "filtersActive"} .small-12.columns = render partial: 'shared/components/filter_controls_shopfront' -.row.filter-box.filter-box-shopfront.animate-hide{"ng-hide" => "filtersActive"} +.row.filter-box.filter-box-shopfront.animate-hide{"ng-show" => "filtersActive"} .small-12.columns .row.tdhead .small-12.medium-6.columns @@ -10,8 +10,9 @@ .light Filter by Category .small-12.medium-6.columns.text-right - = render partial: 'shared/components/filter_controls_shopfront' = render partial: 'shared/components/filter_box_shopfront' + = render partial: 'shared/components/filter_controls_shopfront' + .row .small-12.columns %ul.small-block-grid-2.medium-block-grid-3.large-block-grid-4 From b55bced488711c8e4d8a827e10c41dea0b568b1c Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 12:53:48 +1100 Subject: [PATCH 397/681] Put show hide animation back in for clear button --- .../shared/components/_filter_box_shopfront.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/shared/components/_filter_box_shopfront.html.haml b/app/views/shared/components/_filter_box_shopfront.html.haml index f73d13f69f..7a2aa23296 100644 --- a/app/views/shared/components/_filter_box_shopfront.html.haml +++ b/app/views/shared/components/_filter_box_shopfront.html.haml @@ -1,4 +1,4 @@ -/ %span.animate-show{"ng-show" => "filtersActive && totalActive() > 0"} -%a.button.secondary.tiny{"ng-click" => "clearAll()"} - %i.ofn-i_009-close - Clear all filters +%span.animate-show{"ng-show" => "filtersActive && totalActive() > 0"} + %a.button.secondary.tiny{"ng-click" => "clearAll()"} + %i.ofn-i_009-close + Clear all filters From e8c5af004fce7a10e17548a8b3397baf6f5178f0 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 13:01:49 +1100 Subject: [PATCH 398/681] Add another class so we can see which row is clear filters --- app/views/shared/components/_filter_box.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/components/_filter_box.html.haml b/app/views/shared/components/_filter_box.html.haml index 0256a57ff1..efa8b582f2 100644 --- a/app/views/shared/components/_filter_box.html.haml +++ b/app/views/shared/components/_filter_box.html.haml @@ -1,4 +1,4 @@ -.row.filter-box.animate-show{"ng-show" => "filtersActive && totalActive() > 0"} +.row.filter-box.clear-filters.animate-show{"ng-show" => "filtersActive && totalActive() > 0"} .small-12.columns %a.button.secondary.small.expand{"ng-click" => "clearAll()"} %i.ofn-i_009-close From f36c881f52d197e6f7757e663e13ffc1851d6ba1 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 13:02:07 +1100 Subject: [PATCH 399/681] Add clear filters feature to Producers page --- app/views/producers/_filters.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/producers/_filters.html.haml b/app/views/producers/_filters.html.haml index 00472dc91c..e1cd0ccba9 100644 --- a/app/views/producers/_filters.html.haml +++ b/app/views/producers/_filters.html.haml @@ -13,3 +13,5 @@ %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-6 %taxon-selector{objects: "Enterprises.producers | searchEnterprises:query ", results: "activeTaxons"} + = render partial: 'shared/components/filter_box' + From e9cb7f9565d84a2a56cace74951406ecd77f016e Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 13:02:17 +1100 Subject: [PATCH 400/681] Styling for clear filters row --- app/assets/stylesheets/darkswarm/active_table_search.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/active_table_search.css.sass b/app/assets/stylesheets/darkswarm/active_table_search.css.sass index 614509784e..765759f26d 100644 --- a/app/assets/stylesheets/darkswarm/active_table_search.css.sass +++ b/app/assets/stylesheets/darkswarm/active_table_search.css.sass @@ -13,7 +13,7 @@ @include border-radius(0.25em) margin-top: 2px -.row.filter-box:last-child +.row.filter-box.clear-filters background: transparent margin-top: 1em From b9440309944a0d0dec377f2d1fe7671a0651076a Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 13:11:35 +1100 Subject: [PATCH 401/681] Add some logic for small screen layout --- app/assets/stylesheets/darkswarm/active_table_search.css.sass | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/active_table_search.css.sass b/app/assets/stylesheets/darkswarm/active_table_search.css.sass index 765759f26d..4d9f0d0e4e 100644 --- a/app/assets/stylesheets/darkswarm/active_table_search.css.sass +++ b/app/assets/stylesheets/darkswarm/active_table_search.css.sass @@ -12,6 +12,8 @@ border: 1px solid $clr-blue-light @include border-radius(0.25em) margin-top: 2px + @media all and (max-width: 640px) + margin-bottom: 1em .row.filter-box.clear-filters background: transparent From b0c86f83ee69c5cfdb25f511cc837f9d58f48cc8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 11:04:51 +1100 Subject: [PATCH 402/681] Splitting enterprise edit page into partials --- app/views/admin/enterprises/_form.html.haml | 233 +----------------- .../enterprises/form/_about_us.html.haml | 19 ++ .../admin/enterprises/form/_address.html.haml | 39 +++ .../form/_business_details.html.haml | 12 + .../form/_contact_and_social.html.haml | 56 +++++ .../admin/enterprises/form/_images.html.haml | 21 ++ .../form/_primary_details.html.haml | 79 ++++++ 7 files changed, 232 insertions(+), 227 deletions(-) create mode 100644 app/views/admin/enterprises/form/_about_us.html.haml create mode 100644 app/views/admin/enterprises/form/_address.html.haml create mode 100644 app/views/admin/enterprises/form/_business_details.html.haml create mode 100644 app/views/admin/enterprises/form/_contact_and_social.html.haml create mode 100644 app/views/admin/enterprises/form/_images.html.haml create mode 100644 app/views/admin/enterprises/form/_primary_details.html.haml diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index a5a954d948..e952425e7c 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -1,227 +1,6 @@ -%fieldset.eleven.columns.alpha.no-border-bottom - %legend Primary Details - .row - .alpha.eleven.columns - .three.columns.alpha - = f.label :name - %span.required * - .eight.columns.omega - = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles" } - .row - .alpha.eleven.columns - .three.columns.alpha - = f.label :group_ids, 'Groups' - .with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."} - %a What's this? - - .eight.columns.omega - = f.collection_select :group_ids, EnterpriseGroup.all, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..." - - if spree_current_user.admin? - .row - .three.columns.alpha - =f.label :owner_id, 'Owner' - %span.required * - .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} - %a What's this? - .eight.columns - - owner_email = @enterprise.andand.owner.andand.email || "" - = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email - - .row - .three.columns.alpha - %label Primary Producer - .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} - %a What's this? - .five.columns.omega - = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' -   - = f.label :is_primary_producer, 'Producer' - - if spree_current_user.admin? - .row - .alpha.eleven.columns - .three.columns.alpha - = f.label :sells, 'Sells' - .with-tip{'data-powertip' => "None - enterprise does not sell to customers directly.
    Own - Enterprise sells own products to customers.
    Any - Enterprise can sell own or other enterprises products.
    "} - %a What's this? - .two.columns - = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' -   - = f.label :sells, "None", value: "none" - .two.columns - = f.radio_button :sells, "own", 'ng-model' => 'Enterprise.sells' -   - = f.label :sells, "Own", value: "own" - .four.columns.omega - = f.radio_button :sells, "any", 'ng-model' => 'Enterprise.sells' -   - = f.label :sells, "Any", value: "any" - .row - .three.columns.alpha - %label Visible in search? - .with-tip{'data-powertip' => "Determines whether this enterprise will be visible to customers when searching the site."} - %a What's this? - .two.columns - = f.radio_button :visible, true -   - = f.label :visible, "Visible", :value => "true" - .five.columns.omega - = f.radio_button :visible, false -   - = f.label :visible, "Not Visible", :value => "false" - - if @enterprise.is_distributor - - # TODO: Angularise this - .row - .three.columns.alpha - %label Link to shop front - .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} - %a What's this? - .eight.columns.omega - = main_app.shop_enterprise_url(@enterprise) - -# redo denoting required fields in the whole project - .row - Required fields are denoted with an asterisk ( - %span.required * - ) - -= f.fields_for :address do |af| - %fieldset.eleven.columns.alpha.no-border-bottom - %legend Address - .row - .three.columns.alpha - = af.label :address1 - %span.required * - .eight.columns.omega - = af.text_field :address1, { placeholder: "eg. 123 High Street"} - .row - .alpha.three.columns - = af.label :address2 - .eight.columns.omega - = af.text_field :address2 - .row - .three.columns.alpha - = af.label :city, 'Suburb' - \/ - = af.label :zipcode, 'Postcode' - %span.required * - .four.columns - = af.text_field :city, { placeholder: "eg. Northcote"} - .four.columns.omega - = af.text_field :zipcode, { placeholder: "eg. 3070"} - .row - .three.columns.alpha - = af.label :state_id, 'State' - \/ - = af.label :country_id, 'Country' - %span.required * - .four.columns - = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" - .four.columns.omega - = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" - %fieldset.eleven.columns.alpha.no-border-bottom - %legend Contact Details - -if @enterprise.unconfirmed_email - .alert-box - Email change is pending. - We've sent a confirmation email to - %strong= "#{@enterprise.unconfirmed_email}." - = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.unconfirmed_email } ), method: :post) - %a.close{ href: "#" } × - .row - .alpha.three.columns - = f.label :contact, 'Name' - .omega.eight.columns - = f.text_field :contact, { placeholder: "eg. Gustav Plum"} - .row - .alpha.three.columns - = f.label :email - %span.required * - .omega.eight.columns - = f.text_field :email, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email" } - .row{ ng: { hide: "pristineEmail == null || pristineEmail == Enterprise.email"} } - .alpha.three.columns -   - .omega.eight.columns - Note: A new email address may need to be confirmed prior to use - - .row - .alpha.three.columns - = f.label :phone - .omega.eight.columns - = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} - %fieldset.eleven.columns.alpha.no-border-bottom - %legend Enterprise Details - .row - .alpha.three.columns - = f.label :abn, 'ABN' - .omega.eight.columns - = f.text_field :abn, { placeholder: "eg. 99 123 456 789"} - .row - .alpha.three.columns - = f.label :acn, 'ACN' - .omega.eight.columns - = f.text_field :acn, { placeholder: "eg. 123 456 789"} - .row - .alpha.three.columns - = f.label :website - .omega.eight.columns - = f.text_field :website, { placeholder: "eg. www.truffles.com"} - .row - .alpha.three.columns - = f.label :facebook, 'Facebook' - .omega.eight.columns - = f.text_field :facebook - .row - .alpha.three.columns - = f.label :instagram, 'Instagram' - .omega.eight.columns - = f.text_field :instagram - .row - .alpha.three.columns - = f.label :linkedin, 'LinkedIn' - .omega.eight.columns - = f.text_field :linkedin - .row - .alpha.three.columns - = f.label :twitter - .omega.eight.columns - = f.text_field :twitter, { placeholder: "eg. @the_prof" } -%fieldset.eleven.columns.alpha.no-border-bottom - %legend About Us - .row - .alpha.three.columns - = f.label :description, 'Short Description' - .omega.eight.columns - = f.text_field :description, maxlength: 255, placeholder: 'Tell us about your enterprise in one or two sentences' - .row - .alpha.three.columns - = f.label :long_description, 'About Us' - .omega.eight.columns - -# textAngular toolbar options, add to the ta-toolbar array below and separate into groups with extra ],[ if needed: - -# ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'], - -# ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'], - -# ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], - -# ['html', 'insertImage', 'insertLink', 'insertVideo'] - %text-angular{'ng-model' => 'htmlVariable', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', - 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", - 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} -%fieldset.eleven.columns.alpha.no-border-bottom - %legend IMAGES - .row - .alpha.three.columns - = f.label :logo - %br - 100 x 100 pixels - .omega.eight.columns - = image_tag @object.logo(:medium) 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 in "About Us"' - %br/ - %span{ style: 'font-weight:bold' } PLEASE NOTE: - Any promo image uploaded here will be cropped to 1200 x 260. - The promo image is displayed at the top of an enterprise's profile page and pop-ups. - - .omega.eight.columns - = image_tag @object.promo_image(:large) if @object.promo_image.present? - = f.file_field :promo_image += render 'admin/enterprises/form/primary_details', f: f += render 'admin/enterprises/form/address', f: f += render 'admin/enterprises/form/contact_and_social', f: f += render 'admin/enterprises/form/business_details', f: f += render 'admin/enterprises/form/about_us', f: f += render 'admin/enterprises/form/images', f: f diff --git a/app/views/admin/enterprises/form/_about_us.html.haml b/app/views/admin/enterprises/form/_about_us.html.haml new file mode 100644 index 0000000000..1e9647f30a --- /dev/null +++ b/app/views/admin/enterprises/form/_about_us.html.haml @@ -0,0 +1,19 @@ +%fieldset.eleven.columns.alpha.no-border-bottom + %legend About Us + .row + .alpha.three.columns + = f.label :description, 'Short Description' + .omega.eight.columns + = f.text_field :description, maxlength: 255, placeholder: 'Tell us about your enterprise in one or two sentences' + .row + .alpha.three.columns + = f.label :long_description, 'About Us' + .omega.eight.columns + -# textAngular toolbar options, add to the ta-toolbar array below and separate into groups with extra ],[ if needed: + -# ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'], + -# ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'], + -# ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], + -# ['html', 'insertImage', 'insertLink', 'insertVideo'] + %text-angular{'ng-model' => 'htmlVariable', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', + 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", + 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_address.html.haml b/app/views/admin/enterprises/form/_address.html.haml new file mode 100644 index 0000000000..fc7d42404f --- /dev/null +++ b/app/views/admin/enterprises/form/_address.html.haml @@ -0,0 +1,39 @@ += f.fields_for :address do |af| + %fieldset.eleven.columns.alpha.no-border-bottom + %legend Address + -# redo denoting required fields in the whole project + .row + Required fields are denoted with an asterisk ( + %span.required * + ) + .row + .three.columns.alpha + = af.label :address1 + %span.required * + .eight.columns.omega + = af.text_field :address1, { placeholder: "eg. 123 High Street"} + .row + .alpha.three.columns + = af.label :address2 + .eight.columns.omega + = af.text_field :address2 + .row + .three.columns.alpha + = af.label :city, 'Suburb' + \/ + = af.label :zipcode, 'Postcode' + %span.required * + .four.columns + = af.text_field :city, { placeholder: "eg. Northcote"} + .four.columns.omega + = af.text_field :zipcode, { placeholder: "eg. 3070"} + .row + .three.columns.alpha + = af.label :state_id, 'State' + \/ + = af.label :country_id, 'Country' + %span.required * + .four.columns + = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" + .four.columns.omega + = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_business_details.html.haml b/app/views/admin/enterprises/form/_business_details.html.haml new file mode 100644 index 0000000000..572d96df14 --- /dev/null +++ b/app/views/admin/enterprises/form/_business_details.html.haml @@ -0,0 +1,12 @@ +%fieldset.eleven.columns.alpha.no-border-bottom + %legend Business Details + .row + .alpha.three.columns + = f.label :abn, 'ABN' + .omega.eight.columns + = f.text_field :abn, { placeholder: "eg. 99 123 456 789"} + .row + .alpha.three.columns + = f.label :acn, 'ACN' + .omega.eight.columns + = f.text_field :acn, { placeholder: "eg. 123 456 789"} \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_contact_and_social.html.haml b/app/views/admin/enterprises/form/_contact_and_social.html.haml new file mode 100644 index 0000000000..da264bed90 --- /dev/null +++ b/app/views/admin/enterprises/form/_contact_and_social.html.haml @@ -0,0 +1,56 @@ +%fieldset.eleven.columns.alpha.no-border-bottom + %legend Contact & Social Details + -if @enterprise.unconfirmed_email + .alert-box + Email change is pending. + We've sent a confirmation email to + %strong= "#{@enterprise.unconfirmed_email}." + = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.unconfirmed_email } ), method: :post) + %a.close{ href: "#" } × + .row + .alpha.three.columns + = f.label :contact, 'Name' + .omega.eight.columns + = f.text_field :contact, { placeholder: "eg. Gustav Plum"} + .row + .alpha.three.columns + = f.label :email + %span.required * + .omega.eight.columns + = f.text_field :email, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email" } + .row{ ng: { hide: "pristineEmail == null || pristineEmail == Enterprise.email"} } + .alpha.three.columns +   + .omega.eight.columns + Note: A new email address may need to be confirmed prior to use + + .row + .alpha.three.columns + = f.label :phone + .omega.eight.columns + = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} + .row + .alpha.three.columns + = f.label :website + .omega.eight.columns + = f.text_field :website, { placeholder: "eg. www.truffles.com"} + .row + .alpha.three.columns + = f.label :facebook, 'Facebook' + .omega.eight.columns + = f.text_field :facebook + .row + .alpha.three.columns + = f.label :instagram, 'Instagram' + .omega.eight.columns + = f.text_field :instagram + .row + .alpha.three.columns + = f.label :linkedin, 'LinkedIn' + .omega.eight.columns + = f.text_field :linkedin + .row + .alpha.three.columns + = f.label :twitter + .omega.eight.columns + = f.text_field :twitter, { placeholder: "eg. @the_prof" } \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml new file mode 100644 index 0000000000..881062cd0e --- /dev/null +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -0,0 +1,21 @@ +%fieldset.eleven.columns.alpha.no-border-bottom + %legend IMAGES + .row + .alpha.three.columns + = f.label :logo + %br + 100 x 100 pixels + .omega.eight.columns + = image_tag @object.logo(:medium) 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 in "About Us"' + %br/ + %span{ style: 'font-weight:bold' } PLEASE NOTE: + Any promo image uploaded here will be cropped to 1200 x 260. + The promo image is displayed at the top of an enterprise's profile page and pop-ups. + + .omega.eight.columns + = image_tag @object.promo_image(:large) if @object.promo_image.present? + = f.file_field :promo_image \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml new file mode 100644 index 0000000000..4095fa3a90 --- /dev/null +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -0,0 +1,79 @@ +%fieldset.eleven.columns.alpha.no-border-bottom + %legend Primary Details + .row + .alpha.eleven.columns + .three.columns.alpha + = f.label :name + %span.required * + .eight.columns.omega + = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles" } + .row + .alpha.eleven.columns + .three.columns.alpha + = f.label :group_ids, 'Groups' + .with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."} + %a What's this? + + .eight.columns.omega + = f.collection_select :group_ids, EnterpriseGroup.all, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..." + - if spree_current_user.admin? + .row + .three.columns.alpha + =f.label :owner_id, 'Owner' + %span.required * + .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} + %a What's this? + .eight.columns + - owner_email = @enterprise.andand.owner.andand.email || "" + = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email + + .row + .three.columns.alpha + %label Primary Producer + .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %a What's this? + .five.columns.omega + = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' +   + = f.label :is_primary_producer, 'Producer' + - if spree_current_user.admin? + .row + .alpha.eleven.columns + .three.columns.alpha + = f.label :sells, 'Sells' + .with-tip{'data-powertip' => "None - enterprise does not sell to customers directly.
    Own - Enterprise sells own products to customers.
    Any - Enterprise can sell own or other enterprises products.
    "} + %a What's this? + .two.columns + = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "None", value: "none" + .two.columns + = f.radio_button :sells, "own", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "Own", value: "own" + .four.columns.omega + = f.radio_button :sells, "any", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "Any", value: "any" + .row + .three.columns.alpha + %label Visible in search? + .with-tip{'data-powertip' => "Determines whether this enterprise will be visible to customers when searching the site."} + %a What's this? + .two.columns + = f.radio_button :visible, true +   + = f.label :visible, "Visible", :value => "true" + .five.columns.omega + = f.radio_button :visible, false +   + = f.label :visible, "Not Visible", :value => "false" + - if @enterprise.is_distributor + - # TODO: Angularise this + .row + .three.columns.alpha + %label Link to shop front + .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} + %a What's this? + .eight.columns.omega + = main_app.shop_enterprise_url(@enterprise) \ No newline at end of file From 39ca0ce3dc68ee0970225a00a736062e8af7579c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 12:05:01 +1100 Subject: [PATCH 403/681] WIP: Adding a side_menu to the enterprise form --- app/assets/javascripts/admin/all.js | 1 + .../side_menu_controller.js.coffee | 20 +++++++++ .../admin/enterprises/enterprises.js.coffee | 2 +- .../side_menu/services/side_menu.js.coffee | 15 +++++++ .../admin/side_menu/side_menu.js.coffee | 1 + .../stylesheets/admin/side_menu.css.sass | 17 ++++++++ .../admin/enterprises/_ng_form.html.haml | 6 +-- .../admin/enterprises/_side_menu.html.haml | 3 ++ .../side_menu_controller_spec.js.coffee | 41 +++++++++++++++++++ 9 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee create mode 100644 app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee create mode 100644 app/assets/javascripts/admin/side_menu/side_menu.js.coffee create mode 100644 app/assets/stylesheets/admin/side_menu.css.sass create mode 100644 app/views/admin/enterprises/_side_menu.html.haml create mode 100644 spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 7dc9d1116f..d51898775d 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -22,6 +22,7 @@ //= require ./payment_methods/payment_methods //= require ./products/products //= require ./shipping_methods/shipping_methods +//= require ./side_menu/side_menu //= require ./utils/utils //= require ./users/users //= require textAngular.min.js diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee new file mode 100644 index 0000000000..4076f83ab1 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -0,0 +1,20 @@ +angular.module("admin.enterprises") + .controller "sideMenuCtrl", ($scope, Enterprise, SideMenu) -> + $scope.Enterprise = Enterprise.enterprise + $scope.select = SideMenu.select + + SideMenu.setItems [ + { name: 'Primary Details' } + { name: 'Address' } + { name: "Shipping Methods"} + { name: "Payment Methods"} + { name: "Enterprise Fees"} + { name: 'Contact & Social' } + { name: 'About' } + { name: "Business Details"} + { name: 'Images' } + { name: "Preferences"} + ] + + $scope.select(0) + $scope.menu = SideMenu.items diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index e1e43854d1..1568af32f7 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprises", ["admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular"]) \ No newline at end of file +angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu"] ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee b/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee new file mode 100644 index 0000000000..5f45bb4818 --- /dev/null +++ b/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee @@ -0,0 +1,15 @@ +angular.module("admin.side_menu") + .factory "SideMenu", -> + new class SideMenu + items: [] + selected: null + + setItems: (items) => + @items = items + + select: (index) => + @selected.selected = false if @selected + @selected = @items[index] + @selected.selected = true + + diff --git a/app/assets/javascripts/admin/side_menu/side_menu.js.coffee b/app/assets/javascripts/admin/side_menu/side_menu.js.coffee new file mode 100644 index 0000000000..c0e0c1d766 --- /dev/null +++ b/app/assets/javascripts/admin/side_menu/side_menu.js.coffee @@ -0,0 +1 @@ +angular.module("admin.side_menu", []) \ No newline at end of file diff --git a/app/assets/stylesheets/admin/side_menu.css.sass b/app/assets/stylesheets/admin/side_menu.css.sass new file mode 100644 index 0000000000..540b7ceae9 --- /dev/null +++ b/app/assets/stylesheets/admin/side_menu.css.sass @@ -0,0 +1,17 @@ +.side_menu + border-right: 2px solid #f6f6f6 + border-top: 2px solid #f6f6f6 + .menu_item + padding: 8px 15px + font-size: 120% + cursor: pointer + text-transform: uppercase + &.even + background-color: #ebf3fb + &.odd + background-color: #ffffff + &:hover + background-color: #eaf0f5 + &.selected + background-color: #5498da + color: #ffffff diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index e0a9bef86e..198a66383b 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -11,11 +11,11 @@ } do |f| .row .sixteen.columns.alpha + .four.columns.omega + = render 'side_menu' + .one.column   .eleven.columns.alpha.fullwidth_inputs = render 'form', f: f - .one.column   - .four.columns.omega - = render 'sidebar', f: f .row .twelve.columns.alpha = render partial: "spree/admin/shared/#{action}_resource_links" diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/enterprises/_side_menu.html.haml new file mode 100644 index 0000000000..0f03786527 --- /dev/null +++ b/app/views/admin/enterprises/_side_menu.html.haml @@ -0,0 +1,3 @@ +.side_menu{ ng: { controller: 'sideMenuCtrl' } } + .menu_item{ ng: { repeat: '(index,item) in menu', click: 'select(index)', class: '{ selected: item.selected}', 'class-odd' => "'odd'", 'class-even' => "'even'" } } + {{ item.name }} \ No newline at end of file diff --git a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee new file mode 100644 index 0000000000..87d5fe3169 --- /dev/null +++ b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee @@ -0,0 +1,41 @@ +describe "menuCtrl", -> + ctrl = null + scope = null + Enterprise = null + SideMenu = SideMenu + + beforeEach -> + module('admin.enterprises') + Enterprise = + enterprise: + payment_method_ids: [ 1, 3 ] + shipping_method_ids: [ 2, 4 ] + # PaymentMethods = + # paymentMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] + # ShippingMethods = + # shippingMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] + + inject ($controller, _SideMenu_) -> + scope = {} + SideMenu = _SideMenu_ + spyOn(SideMenu, "select").andCallThrough() + spyOn(SideMenu, "setItems").andCallThrough() + ctrl = $controller 'sideMenuCtrl', {$scope: scope, Enterprise: Enterprise, SideMenu: SideMenu} + + describe "initialisation", -> + it "stores enterprise", -> + expect(scope.Enterprise).toEqual Enterprise.enterprise + + it "sets the item list", -> + expect(SideMenu.setItems).toHaveBeenCalled + expect(scope.menu).toBe SideMenu.items + + it "sets the initally selected value", -> + expect(SideMenu.select).toHaveBeenCalledWith 0 + + + describe "selecting an item", -> + it "selects an item by performing setting the selected property on the item to true", -> + scope.select 4 + expect(SideMenu.select).toHaveBeenCalledWith 4 + expect(scope.menu[4].selected).toBe true From d8349bc037840b190312494c6da23bc2a3b951e8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 12:29:17 +1100 Subject: [PATCH 404/681] menu refers to SideMenu object --- .../enterprises/controllers/side_menu_controller.js.coffee | 2 +- app/views/admin/enterprises/_side_menu.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index 4076f83ab1..5de7cecf3a 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -1,6 +1,7 @@ angular.module("admin.enterprises") .controller "sideMenuCtrl", ($scope, Enterprise, SideMenu) -> $scope.Enterprise = Enterprise.enterprise + $scope.menu = SideMenu $scope.select = SideMenu.select SideMenu.setItems [ @@ -17,4 +18,3 @@ angular.module("admin.enterprises") ] $scope.select(0) - $scope.menu = SideMenu.items diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/enterprises/_side_menu.html.haml index 0f03786527..3022456e4a 100644 --- a/app/views/admin/enterprises/_side_menu.html.haml +++ b/app/views/admin/enterprises/_side_menu.html.haml @@ -1,3 +1,3 @@ .side_menu{ ng: { controller: 'sideMenuCtrl' } } - .menu_item{ ng: { repeat: '(index,item) in menu', click: 'select(index)', class: '{ selected: item.selected}', 'class-odd' => "'odd'", 'class-even' => "'even'" } } + .menu_item{ ng: { repeat: '(index,item) in menu.items', click: 'select(index)', class: '{ selected: item.selected}', 'class-odd' => "'odd'", 'class-even' => "'even'" } } {{ item.name }} \ No newline at end of file From f86d4a19967fd4d7ed79fc6471c0b6e4bc1fb20e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 12:39:24 +1100 Subject: [PATCH 405/681] Pulling headings out of enterprise form partials, displaying each conditionally --- .../enterprise_controller.js.coffee | 3 +- app/views/admin/enterprises/_form.html.haml | 30 +++- .../enterprises/form/_about_us.html.haml | 36 +++-- .../admin/enterprises/form/_address.html.haml | 75 +++++---- .../form/_business_details.html.haml | 22 ++- .../form/_contact_and_social.html.haml | 108 +++++++------ .../admin/enterprises/form/_images.html.haml | 38 +++-- .../form/_primary_details.html.haml | 142 +++++++++--------- .../side_menu_controller_spec.js.coffee | 4 +- 9 files changed, 232 insertions(+), 226 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 4c14a3b096..de63fa4d05 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,5 +1,5 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, longDescription, NavigationCheck, Enterprise, PaymentMethods, ShippingMethods) -> + .controller "enterpriseCtrl", ($scope, longDescription, NavigationCheck, Enterprise, PaymentMethods, ShippingMethods, SideMenu) -> $scope.Enterprise = Enterprise.enterprise $scope.PaymentMethods = PaymentMethods.paymentMethods $scope.ShippingMethods = ShippingMethods.shippingMethods @@ -7,6 +7,7 @@ angular.module("admin.enterprises") # htmlVariable is used by textAngular wysiwyg for the long descrtiption. $scope.htmlVariable = longDescription $scope.pristineEmail = $scope.Enterprise.email + $scope.menu = SideMenu # Provide a callback for generating warning messages displayed before leaving the page. This is passed in # from a directive "nav-check" in the page - if we pass it here it will be called in the test suite, diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index e952425e7c..a8fae9c944 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -1,6 +1,24 @@ -= render 'admin/enterprises/form/primary_details', f: f -= render 'admin/enterprises/form/address', f: f -= render 'admin/enterprises/form/contact_and_social', f: f -= render 'admin/enterprises/form/business_details', f: f -= render 'admin/enterprises/form/about_us', f: f -= render 'admin/enterprises/form/images', f: f +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Primary Details'" } } + %legend Primary Details + = render 'admin/enterprises/form/primary_details', f: f + += f.fields_for :address do |af| + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Address'" } } + %legend Address + = render 'admin/enterprises/form/address', af: af + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact & Social'" } } + %legend Contact & Social + = render 'admin/enterprises/form/contact_and_social', f: f + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Business Details'" } } + %legend Business Details + = render 'admin/enterprises/form/business_details', f: f + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } + %legend About Us + = render 'admin/enterprises/form/about_us', f: f + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Images'" } } + %legend Images + = render 'admin/enterprises/form/images', f: f diff --git a/app/views/admin/enterprises/form/_about_us.html.haml b/app/views/admin/enterprises/form/_about_us.html.haml index 1e9647f30a..e00be48828 100644 --- a/app/views/admin/enterprises/form/_about_us.html.haml +++ b/app/views/admin/enterprises/form/_about_us.html.haml @@ -1,19 +1,17 @@ -%fieldset.eleven.columns.alpha.no-border-bottom - %legend About Us - .row - .alpha.three.columns - = f.label :description, 'Short Description' - .omega.eight.columns - = f.text_field :description, maxlength: 255, placeholder: 'Tell us about your enterprise in one or two sentences' - .row - .alpha.three.columns - = f.label :long_description, 'About Us' - .omega.eight.columns - -# textAngular toolbar options, add to the ta-toolbar array below and separate into groups with extra ],[ if needed: - -# ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'], - -# ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'], - -# ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], - -# ['html', 'insertImage', 'insertLink', 'insertVideo'] - %text-angular{'ng-model' => 'htmlVariable', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', - 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", - 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} \ No newline at end of file +.row + .alpha.three.columns + = f.label :description, 'Short Description' + .omega.eight.columns + = f.text_field :description, maxlength: 255, placeholder: 'Tell us about your enterprise in one or two sentences' +.row + .alpha.three.columns + = f.label :long_description, 'About Us' + .omega.eight.columns + -# textAngular toolbar options, add to the ta-toolbar array below and separate into groups with extra ],[ if needed: + -# ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'], + -# ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'], + -# ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], + -# ['html', 'insertImage', 'insertLink', 'insertVideo'] + %text-angular{'ng-model' => 'htmlVariable', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', + 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", + 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_address.html.haml b/app/views/admin/enterprises/form/_address.html.haml index fc7d42404f..68ae7d68bf 100644 --- a/app/views/admin/enterprises/form/_address.html.haml +++ b/app/views/admin/enterprises/form/_address.html.haml @@ -1,39 +1,36 @@ -= f.fields_for :address do |af| - %fieldset.eleven.columns.alpha.no-border-bottom - %legend Address - -# redo denoting required fields in the whole project - .row - Required fields are denoted with an asterisk ( - %span.required * - ) - .row - .three.columns.alpha - = af.label :address1 - %span.required * - .eight.columns.omega - = af.text_field :address1, { placeholder: "eg. 123 High Street"} - .row - .alpha.three.columns - = af.label :address2 - .eight.columns.omega - = af.text_field :address2 - .row - .three.columns.alpha - = af.label :city, 'Suburb' - \/ - = af.label :zipcode, 'Postcode' - %span.required * - .four.columns - = af.text_field :city, { placeholder: "eg. Northcote"} - .four.columns.omega - = af.text_field :zipcode, { placeholder: "eg. 3070"} - .row - .three.columns.alpha - = af.label :state_id, 'State' - \/ - = af.label :country_id, 'Country' - %span.required * - .four.columns - = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" - .four.columns.omega - = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" \ No newline at end of file +-# redo denoting required fields in the whole project +.row + Required fields are denoted with an asterisk ( + %span.required * + ) +.row + .three.columns.alpha + = af.label :address1 + %span.required * + .eight.columns.omega + = af.text_field :address1, { placeholder: "eg. 123 High Street"} +.row + .alpha.three.columns + = af.label :address2 + .eight.columns.omega + = af.text_field :address2 +.row + .three.columns.alpha + = af.label :city, 'Suburb' + \/ + = af.label :zipcode, 'Postcode' + %span.required * + .four.columns + = af.text_field :city, { placeholder: "eg. Northcote"} + .four.columns.omega + = af.text_field :zipcode, { placeholder: "eg. 3070"} +.row + .three.columns.alpha + = af.label :state_id, 'State' + \/ + = af.label :country_id, 'Country' + %span.required * + .four.columns + = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" + .four.columns.omega + = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" diff --git a/app/views/admin/enterprises/form/_business_details.html.haml b/app/views/admin/enterprises/form/_business_details.html.haml index 572d96df14..7fb4d2ccb3 100644 --- a/app/views/admin/enterprises/form/_business_details.html.haml +++ b/app/views/admin/enterprises/form/_business_details.html.haml @@ -1,12 +1,10 @@ -%fieldset.eleven.columns.alpha.no-border-bottom - %legend Business Details - .row - .alpha.three.columns - = f.label :abn, 'ABN' - .omega.eight.columns - = f.text_field :abn, { placeholder: "eg. 99 123 456 789"} - .row - .alpha.three.columns - = f.label :acn, 'ACN' - .omega.eight.columns - = f.text_field :acn, { placeholder: "eg. 123 456 789"} \ No newline at end of file +.row + .alpha.three.columns + = f.label :abn, 'ABN' + .omega.eight.columns + = f.text_field :abn, { placeholder: "eg. 99 123 456 789"} +.row + .alpha.three.columns + = f.label :acn, 'ACN' + .omega.eight.columns + = f.text_field :acn, { placeholder: "eg. 123 456 789"} \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_contact_and_social.html.haml b/app/views/admin/enterprises/form/_contact_and_social.html.haml index da264bed90..6f489835ec 100644 --- a/app/views/admin/enterprises/form/_contact_and_social.html.haml +++ b/app/views/admin/enterprises/form/_contact_and_social.html.haml @@ -1,56 +1,54 @@ -%fieldset.eleven.columns.alpha.no-border-bottom - %legend Contact & Social Details - -if @enterprise.unconfirmed_email - .alert-box - Email change is pending. - We've sent a confirmation email to - %strong= "#{@enterprise.unconfirmed_email}." - = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.unconfirmed_email } ), method: :post) - %a.close{ href: "#" } × - .row - .alpha.three.columns - = f.label :contact, 'Name' - .omega.eight.columns - = f.text_field :contact, { placeholder: "eg. Gustav Plum"} - .row - .alpha.three.columns - = f.label :email - %span.required * - .omega.eight.columns - = f.text_field :email, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email" } - .row{ ng: { hide: "pristineEmail == null || pristineEmail == Enterprise.email"} } - .alpha.three.columns -   - .omega.eight.columns - Note: A new email address may need to be confirmed prior to use +-if @enterprise.unconfirmed_email + .alert-box + Email change is pending. + We've sent a confirmation email to + %strong= "#{@enterprise.unconfirmed_email}." + = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.unconfirmed_email } ), method: :post) + %a.close{ href: "#" } × +.row + .alpha.three.columns + = f.label :contact, 'Name' + .omega.eight.columns + = f.text_field :contact, { placeholder: "eg. Gustav Plum"} +.row + .alpha.three.columns + = f.label :email + %span.required * + .omega.eight.columns + = f.text_field :email, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email" } +.row{ ng: { hide: "pristineEmail == null || pristineEmail == Enterprise.email"} } + .alpha.three.columns +   + .omega.eight.columns + Note: A new email address may need to be confirmed prior to use - .row - .alpha.three.columns - = f.label :phone - .omega.eight.columns - = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} - .row - .alpha.three.columns - = f.label :website - .omega.eight.columns - = f.text_field :website, { placeholder: "eg. www.truffles.com"} - .row - .alpha.three.columns - = f.label :facebook, 'Facebook' - .omega.eight.columns - = f.text_field :facebook - .row - .alpha.three.columns - = f.label :instagram, 'Instagram' - .omega.eight.columns - = f.text_field :instagram - .row - .alpha.three.columns - = f.label :linkedin, 'LinkedIn' - .omega.eight.columns - = f.text_field :linkedin - .row - .alpha.three.columns - = f.label :twitter - .omega.eight.columns - = f.text_field :twitter, { placeholder: "eg. @the_prof" } \ No newline at end of file +.row + .alpha.three.columns + = f.label :phone + .omega.eight.columns + = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} +.row + .alpha.three.columns + = f.label :website + .omega.eight.columns + = f.text_field :website, { placeholder: "eg. www.truffles.com"} +.row + .alpha.three.columns + = f.label :facebook, 'Facebook' + .omega.eight.columns + = f.text_field :facebook +.row + .alpha.three.columns + = f.label :instagram, 'Instagram' + .omega.eight.columns + = f.text_field :instagram +.row + .alpha.three.columns + = f.label :linkedin, 'LinkedIn' + .omega.eight.columns + = f.text_field :linkedin +.row + .alpha.three.columns + = f.label :twitter + .omega.eight.columns + = f.text_field :twitter, { placeholder: "eg. @the_prof" } \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml index 881062cd0e..ac960ccc1e 100644 --- a/app/views/admin/enterprises/form/_images.html.haml +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -1,21 +1,19 @@ -%fieldset.eleven.columns.alpha.no-border-bottom - %legend IMAGES - .row - .alpha.three.columns - = f.label :logo - %br - 100 x 100 pixels - .omega.eight.columns - = image_tag @object.logo(:medium) 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 in "About Us"' - %br/ - %span{ style: 'font-weight:bold' } PLEASE NOTE: - Any promo image uploaded here will be cropped to 1200 x 260. - The promo image is displayed at the top of an enterprise's profile page and pop-ups. +.row + .alpha.three.columns + = f.label :logo + %br + 100 x 100 pixels + .omega.eight.columns + = image_tag @object.logo(:medium) 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 in "About Us"' + %br/ + %span{ style: 'font-weight:bold' } PLEASE NOTE: + Any promo image uploaded here will be cropped to 1200 x 260. + The promo image is displayed at the top of an enterprise's profile page and pop-ups. - .omega.eight.columns - = image_tag @object.promo_image(:large) if @object.promo_image.present? - = f.file_field :promo_image \ No newline at end of file + .omega.eight.columns + = image_tag @object.promo_image(:large) if @object.promo_image.present? + = f.file_field :promo_image \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml index 4095fa3a90..91d8661518 100644 --- a/app/views/admin/enterprises/form/_primary_details.html.haml +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -1,79 +1,77 @@ -%fieldset.eleven.columns.alpha.no-border-bottom - %legend Primary Details +.row + .alpha.eleven.columns + .three.columns.alpha + = f.label :name + %span.required * + .eight.columns.omega + = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles" } +.row + .alpha.eleven.columns + .three.columns.alpha + = f.label :group_ids, 'Groups' + .with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."} + %a What's this? + + .eight.columns.omega + = f.collection_select :group_ids, EnterpriseGroup.all, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..." +- if spree_current_user.admin? + .row + .three.columns.alpha + =f.label :owner_id, 'Owner' + %span.required * + .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} + %a What's this? + .eight.columns + - owner_email = @enterprise.andand.owner.andand.email || "" + = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email + +.row + .three.columns.alpha + %label Primary Producer + .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %a What's this? + .five.columns.omega + = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' +   + = f.label :is_primary_producer, 'Producer' +- if spree_current_user.admin? .row .alpha.eleven.columns .three.columns.alpha - = f.label :name - %span.required * - .eight.columns.omega - = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles" } - .row - .alpha.eleven.columns - .three.columns.alpha - = f.label :group_ids, 'Groups' - .with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."} + = f.label :sells, 'Sells' + .with-tip{'data-powertip' => "None - enterprise does not sell to customers directly.
    Own - Enterprise sells own products to customers.
    Any - Enterprise can sell own or other enterprises products.
    "} %a What's this? - - .eight.columns.omega - = f.collection_select :group_ids, EnterpriseGroup.all, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..." - - if spree_current_user.admin? - .row - .three.columns.alpha - =f.label :owner_id, 'Owner' - %span.required * - .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} - %a What's this? - .eight.columns - - owner_email = @enterprise.andand.owner.andand.email || "" - = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email - + .two.columns + = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "None", value: "none" + .two.columns + = f.radio_button :sells, "own", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "Own", value: "own" + .four.columns.omega + = f.radio_button :sells, "any", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "Any", value: "any" +.row + .three.columns.alpha + %label Visible in search? + .with-tip{'data-powertip' => "Determines whether this enterprise will be visible to customers when searching the site."} + %a What's this? + .two.columns + = f.radio_button :visible, true +   + = f.label :visible, "Visible", :value => "true" + .five.columns.omega + = f.radio_button :visible, false +   + = f.label :visible, "Not Visible", :value => "false" +- if @enterprise.is_distributor +- # TODO: Angularise this .row .three.columns.alpha - %label Primary Producer - .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %label Link to shop front + .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} %a What's this? - .five.columns.omega - = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' -   - = f.label :is_primary_producer, 'Producer' - - if spree_current_user.admin? - .row - .alpha.eleven.columns - .three.columns.alpha - = f.label :sells, 'Sells' - .with-tip{'data-powertip' => "None - enterprise does not sell to customers directly.
    Own - Enterprise sells own products to customers.
    Any - Enterprise can sell own or other enterprises products.
    "} - %a What's this? - .two.columns - = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' -   - = f.label :sells, "None", value: "none" - .two.columns - = f.radio_button :sells, "own", 'ng-model' => 'Enterprise.sells' -   - = f.label :sells, "Own", value: "own" - .four.columns.omega - = f.radio_button :sells, "any", 'ng-model' => 'Enterprise.sells' -   - = f.label :sells, "Any", value: "any" - .row - .three.columns.alpha - %label Visible in search? - .with-tip{'data-powertip' => "Determines whether this enterprise will be visible to customers when searching the site."} - %a What's this? - .two.columns - = f.radio_button :visible, true -   - = f.label :visible, "Visible", :value => "true" - .five.columns.omega - = f.radio_button :visible, false -   - = f.label :visible, "Not Visible", :value => "false" - - if @enterprise.is_distributor - - # TODO: Angularise this - .row - .three.columns.alpha - %label Link to shop front - .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} - %a What's this? - .eight.columns.omega - = main_app.shop_enterprise_url(@enterprise) \ No newline at end of file + .eight.columns.omega + = main_app.shop_enterprise_url(@enterprise) \ No newline at end of file diff --git a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee index 87d5fe3169..5261a96390 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee @@ -28,7 +28,7 @@ describe "menuCtrl", -> it "sets the item list", -> expect(SideMenu.setItems).toHaveBeenCalled - expect(scope.menu).toBe SideMenu.items + expect(scope.menu.items).toBe SideMenu.items it "sets the initally selected value", -> expect(SideMenu.select).toHaveBeenCalledWith 0 @@ -38,4 +38,4 @@ describe "menuCtrl", -> it "selects an item by performing setting the selected property on the item to true", -> scope.select 4 expect(SideMenu.select).toHaveBeenCalledWith 4 - expect(scope.menu[4].selected).toBe true + expect(scope.menu.items[4].selected).toBe true From b10d623f26fe2afbfc7d2d5f4149880ebe18e4a2 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 12:42:18 +1100 Subject: [PATCH 406/681] Rearrange columns for action buttons on enterprise form --- app/views/admin/enterprises/_ng_form.html.haml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 198a66383b..76ec768a3a 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -11,11 +11,13 @@ } do |f| .row .sixteen.columns.alpha - .four.columns.omega + .four.columns.alpha = render 'side_menu' .one.column   - .eleven.columns.alpha.fullwidth_inputs + .eleven.columns.omega.fullwidth_inputs = render 'form', f: f .row - .twelve.columns.alpha + .five.columns.alpha +   + .eleven.columns.alpha = render partial: "spree/admin/shared/#{action}_resource_links" From eb2a6f0ef52a7af91b6d924b4cc77f43004ff881 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 12:57:09 +1100 Subject: [PATCH 407/681] Splitting contact and social into two separate partials --- .../side_menu_controller.js.coffee | 9 ++++---- app/views/admin/enterprises/_form.html.haml | 10 +++++--- ...nd_social.html.haml => _contact.html.haml} | 23 +------------------ .../admin/enterprises/form/_social.html.haml | 20 ++++++++++++++++ 4 files changed, 33 insertions(+), 29 deletions(-) rename app/views/admin/enterprises/form/{_contact_and_social.html.haml => _contact.html.haml} (70%) create mode 100644 app/views/admin/enterprises/form/_social.html.haml diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index 5de7cecf3a..683d1f3bc6 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -7,13 +7,14 @@ angular.module("admin.enterprises") SideMenu.setItems [ { name: 'Primary Details' } { name: 'Address' } - { name: "Shipping Methods"} - { name: "Payment Methods"} - { name: "Enterprise Fees"} - { name: 'Contact & Social' } + { name: 'Contact' } + { name: 'Social' } { name: 'About' } { name: "Business Details"} { name: 'Images' } + { name: "Shipping Methods"} + { name: "Payment Methods"} + { name: "Enterprise Fees"} { name: "Preferences"} ] diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index a8fae9c944..b9a897b774 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -7,9 +7,13 @@ %legend Address = render 'admin/enterprises/form/address', af: af -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact & Social'" } } - %legend Contact & Social - = render 'admin/enterprises/form/contact_and_social', f: f +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } + %legend Contact + = render 'admin/enterprises/form/contact', f: f + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Social'" } } + %legend Social + = render 'admin/enterprises/form/social', f: f %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Business Details'" } } %legend Business Details diff --git a/app/views/admin/enterprises/form/_contact_and_social.html.haml b/app/views/admin/enterprises/form/_contact.html.haml similarity index 70% rename from app/views/admin/enterprises/form/_contact_and_social.html.haml rename to app/views/admin/enterprises/form/_contact.html.haml index 6f489835ec..c16f93f4b0 100644 --- a/app/views/admin/enterprises/form/_contact_and_social.html.haml +++ b/app/views/admin/enterprises/form/_contact.html.haml @@ -21,7 +21,6 @@   .omega.eight.columns Note: A new email address may need to be confirmed prior to use - .row .alpha.three.columns = f.label :phone @@ -31,24 +30,4 @@ .alpha.three.columns = f.label :website .omega.eight.columns - = f.text_field :website, { placeholder: "eg. www.truffles.com"} -.row - .alpha.three.columns - = f.label :facebook, 'Facebook' - .omega.eight.columns - = f.text_field :facebook -.row - .alpha.three.columns - = f.label :instagram, 'Instagram' - .omega.eight.columns - = f.text_field :instagram -.row - .alpha.three.columns - = f.label :linkedin, 'LinkedIn' - .omega.eight.columns - = f.text_field :linkedin -.row - .alpha.three.columns - = f.label :twitter - .omega.eight.columns - = f.text_field :twitter, { placeholder: "eg. @the_prof" } \ No newline at end of file + = f.text_field :website, { placeholder: "eg. www.truffles.com"} \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_social.html.haml b/app/views/admin/enterprises/form/_social.html.haml new file mode 100644 index 0000000000..1909a6fc4a --- /dev/null +++ b/app/views/admin/enterprises/form/_social.html.haml @@ -0,0 +1,20 @@ +.row + .alpha.three.columns + = f.label :facebook, 'Facebook' + .omega.eight.columns + = f.text_field :facebook +.row + .alpha.three.columns + = f.label :instagram, 'Instagram' + .omega.eight.columns + = f.text_field :instagram +.row + .alpha.three.columns + = f.label :linkedin, 'LinkedIn' + .omega.eight.columns + = f.text_field :linkedin +.row + .alpha.three.columns + = f.label :twitter + .omega.eight.columns + = f.text_field :twitter, { placeholder: "eg. @the_prof" } \ No newline at end of file From 76acd706ea4135842e042f845d94764ad73c3d2a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 14:38:28 +1100 Subject: [PATCH 408/681] Adding ability to show and hide menu items --- .../enterprise_controller.js.coffee | 8 ++ .../side_menu_controller.js.coffee | 12 +-- .../side_menu/services/side_menu.js.coffee | 19 +++- .../stylesheets/admin/side_menu.css.sass | 4 +- .../admin/enterprises/_side_menu.html.haml | 7 +- .../side_menu_controller_spec.js.coffee | 4 +- .../side_menu/services/side_menu.js.coffee | 93 +++++++++++++++++++ 7 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 spec/javascripts/unit/admin/side_menu/services/side_menu.js.coffee diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index de63fa4d05..9a665571b2 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -45,3 +45,11 @@ angular.module("admin.enterprises") count++ if shipping_method.selected count , 0 + + $scope.$watch "Enterprise.is_primary_producer", (newValue, oldValue) -> + if newValue + $scope.menu.hide_item_by_name('Shipping Methods') + $scope.menu.hide_item_by_name('Payment Methods') + else if !newValue + $scope.menu.show_item_by_name('Shipping Methods') + $scope.menu.show_item_by_name('Payment Methods') diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index 683d1f3bc6..590728d19f 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -4,18 +4,18 @@ angular.module("admin.enterprises") $scope.menu = SideMenu $scope.select = SideMenu.select - SideMenu.setItems [ + $scope.menu.setItems [ { name: 'Primary Details' } { name: 'Address' } { name: 'Contact' } { name: 'Social' } { name: 'About' } - { name: "Business Details"} + { name: 'Business Details' } { name: 'Images' } - { name: "Shipping Methods"} - { name: "Payment Methods"} - { name: "Enterprise Fees"} - { name: "Preferences"} + { name: "Shipping Methods" } + { name: "Payment Methods" } + { name: "Enterprise Fees" } + { name: "Preferences" } ] $scope.select(0) diff --git a/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee b/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee index 5f45bb4818..020f0981d8 100644 --- a/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee +++ b/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee @@ -6,10 +6,23 @@ angular.module("admin.side_menu") setItems: (items) => @items = items + item.visible = true for item in @items select: (index) => - @selected.selected = false if @selected - @selected = @items[index] - @selected.selected = true + if index < @items.length + @selected.selected = false if @selected + @selected = @items[index] + @selected.selected = true + find_by_name: (name) => + for item in @items when item.name is name + return item + null + hide_item_by_name: (name) => + item = @find_by_name(name) + item.visible = false if item + + show_item_by_name: (name) => + item = @find_by_name(name) + item.visible = true if item diff --git a/app/assets/stylesheets/admin/side_menu.css.sass b/app/assets/stylesheets/admin/side_menu.css.sass index 540b7ceae9..a41e60f711 100644 --- a/app/assets/stylesheets/admin/side_menu.css.sass +++ b/app/assets/stylesheets/admin/side_menu.css.sass @@ -6,9 +6,9 @@ font-size: 120% cursor: pointer text-transform: uppercase - &.even - background-color: #ebf3fb &.odd + background-color: #ebf3fb + &.even background-color: #ffffff &:hover background-color: #eaf0f5 diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/enterprises/_side_menu.html.haml index 3022456e4a..687c2d2a12 100644 --- a/app/views/admin/enterprises/_side_menu.html.haml +++ b/app/views/admin/enterprises/_side_menu.html.haml @@ -1,3 +1,8 @@ .side_menu{ ng: { controller: 'sideMenuCtrl' } } - .menu_item{ ng: { repeat: '(index,item) in menu.items', click: 'select(index)', class: '{ selected: item.selected}', 'class-odd' => "'odd'", 'class-even' => "'even'" } } + .menu_item{ ng: { repeat: '(index,item) in menu.items', + show: "item.visible", + click: 'select(index)', + class: '{ selected: item.selected}', + 'class-odd' => "'odd'", + 'class-even' => "'even'" } } {{ item.name }} \ No newline at end of file diff --git a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee index 5261a96390..136203a644 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee @@ -15,8 +15,8 @@ describe "menuCtrl", -> # ShippingMethods = # shippingMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] - inject ($controller, _SideMenu_) -> - scope = {} + inject ($rootScope, $controller, _SideMenu_) -> + scope = $rootScope SideMenu = _SideMenu_ spyOn(SideMenu, "select").andCallThrough() spyOn(SideMenu, "setItems").andCallThrough() diff --git a/spec/javascripts/unit/admin/side_menu/services/side_menu.js.coffee b/spec/javascripts/unit/admin/side_menu/services/side_menu.js.coffee new file mode 100644 index 0000000000..1c6044b66e --- /dev/null +++ b/spec/javascripts/unit/admin/side_menu/services/side_menu.js.coffee @@ -0,0 +1,93 @@ +describe "SideMenu service", -> + SideMenu = null + + beforeEach -> + module "admin.side_menu" + + beforeEach inject (_SideMenu_) -> + SideMenu = _SideMenu_ + + describe "setting items", -> + it "sets the items", -> + items = [ { name: "Name 1"}, { name: "Name 2"} ] + SideMenu.setItems items + expect(SideMenu.items[0]).toBe items[0] + + it "sets the the visible flag to true for each", -> + items = [ { name: "Name 1"}, { name: "Name 2"} ] + SideMenu.setItems items + expect(items[0].visible).toBe true + + describe "selecting an item", -> + describe "when no item has been selected", -> + it "doesn't crash because of no selected item existing", -> + SideMenu.items = [ { name: "Name 1"}, { name: "Name 2"} ] + SideMenu.select(1) + + it "sets selected to the new item", -> + SideMenu.items = [ { name: "Name 1"}, { name: "Name 2"} ] + SideMenu.select(1) + expect(SideMenu.find_by_name("Name 2")).toBe SideMenu.items[1] + + it "switches the selected value of the newly selected item to true", -> + item1 = { name: "Name 1", selected: false } + item2 = { name: "Name 2", selected: false } + SideMenu.items = [ item1, item2 ] + SideMenu.select(1) + expect(item2.selected).toBe true + + it "doesn't crash if given an index greater than the length of items", -> + SideMenu.items = [ { name: "Name 1"}, { name: "Name 2"} ] + SideMenu.select(12) + + describe "when an item has been selected", -> + item1 = item2 = null + + beforeEach -> + item1 = { name: "Name 1", selected: true } + item2 = { name: "Name 2", selected: false } + SideMenu.selected = item1 + SideMenu.items = [ item1, item2 ] + SideMenu.select(1) + + it "switches the selected value of the existing selected item to false", -> + expect(item1.selected).toBe false + + it "switches the selected value of the newly selected item to true", -> + expect(item2.selected).toBe true + + + describe "finding by name", -> + it "returns the element that matches", -> + SideMenu.items = [ { name: "Name 1"}, { name: "Name 2"} ] + expect(SideMenu.find_by_name("Name 2")).toBe SideMenu.items[1] + + it "returns one element even if two items are found", -> + SideMenu.items = [ { name: "Name 1"}, { name: "Name 1"} ] + expect(SideMenu.find_by_name("Name 1")).toBe SideMenu.items[0] + + it "returns null if no items are found", -> + SideMenu.items = [ { name: "Name 1"}, { name: "Name 2"} ] + expect(SideMenu.find_by_name("Name 3")).toBe null + + describe "hiding an item by name", -> + it "sets visible to false on the response from find_by_name", -> + mockItem = { visible: true } + spyOn(SideMenu, 'find_by_name').andReturn mockItem + SideMenu.hide_item_by_name() + expect(mockItem.visible).toBe false + + it "doesn't crash if null is returned from find_by_name", -> + spyOn(SideMenu, 'find_by_name').andReturn null + SideMenu.hide_item_by_name() + + describe "showing an item by name", -> + it "sets visible to false on the response from find_by_name", -> + mockItem = { visible: false } + spyOn(SideMenu, 'find_by_name').andReturn mockItem + SideMenu.show_item_by_name() + expect(mockItem.visible).toBe true + + it "doesn't crash if null is returned from find_by_name", -> + spyOn(SideMenu, 'find_by_name').andReturn null + SideMenu.show_item_by_name() From 468e83cef7c6c1140c97be6621d5437f27d4ca30 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 16:10:31 +1100 Subject: [PATCH 409/681] Adding global centering css class --- app/assets/stylesheets/admin/openfoodnetwork.css.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 6df859fd09..2919c06f6d 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -1,3 +1,7 @@ +.text-center { + text-align: center; +} + table .blank-action { display: inline-block; width: 29px; From 0ea1adfdff5fa30e758ab7135a86a9911eea55b9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 16:11:15 +1100 Subject: [PATCH 410/681] Adding a partial for enterprise fees to enterprise form --- app/views/admin/enterprises/_form.html.haml | 4 +++ .../form/_enterprise_fees.html.haml | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 app/views/admin/enterprises/form/_enterprise_fees.html.haml diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index b9a897b774..87a43562f3 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -26,3 +26,7 @@ %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Images'" } } %legend Images = render 'admin/enterprises/form/images', f: f + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Enterprise Fees'" } } + %legend Enterprise Fees + = render 'admin/enterprises/form/enterprise_fees', f: f diff --git a/app/views/admin/enterprises/form/_enterprise_fees.html.haml b/app/views/admin/enterprises/form/_enterprise_fees.html.haml new file mode 100644 index 0000000000..c5f83b6ffe --- /dev/null +++ b/app/views/admin/enterprises/form/_enterprise_fees.html.haml @@ -0,0 +1,28 @@ +- if @enterprise_fees.count > 0 + %table + %tr + %th Name + %th Fee Type + -# %th Calculator + -# %th Calculator Values + - @enterprise_fees.each do |enterprise_fee| + %tr + %td= enterprise_fee.name + %td= enterprise_fee.fee_type + -# %td= enterprise_fee.calculator.preferred_flat_percent + -# %td= enterprise_fee.fee_type + %br + %div + %a.button{ href: "#{main_app.admin_enterprise_fees_path}"} + Manage Enterprise Fees + %i.icon-arrow-right + +- else + %p.text-center + You don't have any enterprise fees yet. + + %br + .text-center + %a.button{ href: "#{main_app.admin_enterprise_fees_path}"} + Create One Now + %i.icon-arrow-right From bc32a053f725cb8757e148ddc03d916f72d86d17 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 16:29:45 +1100 Subject: [PATCH 411/681] Adding global styling class for aligning text to the right --- app/assets/stylesheets/admin/openfoodnetwork.css.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 2919c06f6d..62376cb052 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -2,6 +2,10 @@ text-align: center; } +.text-right{ + text-align: right; +} + table .blank-action { display: inline-block; width: 29px; From 25e608d9fb81f536778bb1b98ddc996aed30286f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 16:30:09 +1100 Subject: [PATCH 412/681] Adding shipping methods partial to redone enterprise form --- app/views/admin/enterprises/_form.html.haml | 4 +++ .../form/_shipping_methods.html.haml | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 app/views/admin/enterprises/form/_shipping_methods.html.haml diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 87a43562f3..21789eb66e 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -27,6 +27,10 @@ %legend Images = render 'admin/enterprises/form/images', f: f +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Shipping Methods'" } } + %legend Shipping Methods + = render 'admin/enterprises/form/shipping_methods', f: f + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Enterprise Fees'" } } %legend Enterprise Fees = render 'admin/enterprises/form/enterprise_fees', f: f diff --git a/app/views/admin/enterprises/form/_shipping_methods.html.haml b/app/views/admin/enterprises/form/_shipping_methods.html.haml new file mode 100644 index 0000000000..2d7376abc8 --- /dev/null +++ b/app/views/admin/enterprises/form/_shipping_methods.html.haml @@ -0,0 +1,33 @@ +- if @shipping_methods.count > 0 + %table + %thead + %tr + %th Name + %th Applies? + %th + - @shipping_methods.each do |shipping_method| + %tbody + %tr{ ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } + %td= shipping_method.name + %td= f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil + %td= link_to "Edit", edit_admin_shipping_method_path(shipping_method) + %br + .row + .six.columns.alpha + %a.button{ href: "#{admin_shipping_methods_path}"} + Manage Shipping Methods + %i.icon-arrow-right + .five.columns.omega.text-right + %a.button{ href: "#{new_admin_shipping_method_path}"} + Create New Shipping Method + %i.icon-plus + +- else + %p.text-center + You don't have any shipping methods yet. + + %br + .text-center + %a.button{ href: "#{new_admin_shipping_method_path}"} + Create One Now + %i.icon-arrow-right From 412b431f3650cccea14a7158ac71495df3271809 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 16:37:56 +1100 Subject: [PATCH 413/681] Fixing up tbody positions --- .../form/_enterprise_fees.html.haml | 22 ++++++++++--------- .../form/_shipping_methods.html.haml | 10 ++++----- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/views/admin/enterprises/form/_enterprise_fees.html.haml b/app/views/admin/enterprises/form/_enterprise_fees.html.haml index c5f83b6ffe..53e48f1be9 100644 --- a/app/views/admin/enterprises/form/_enterprise_fees.html.haml +++ b/app/views/admin/enterprises/form/_enterprise_fees.html.haml @@ -1,16 +1,18 @@ - if @enterprise_fees.count > 0 %table - %tr - %th Name - %th Fee Type - -# %th Calculator - -# %th Calculator Values - - @enterprise_fees.each do |enterprise_fee| + %thead %tr - %td= enterprise_fee.name - %td= enterprise_fee.fee_type - -# %td= enterprise_fee.calculator.preferred_flat_percent - -# %td= enterprise_fee.fee_type + %th Name + %th Fee Type + -# %th Calculator + -# %th Calculator Values + %tbody + - @enterprise_fees.each do |enterprise_fee| + %tr + %td= enterprise_fee.name + %td= enterprise_fee.fee_type + -# %td= enterprise_fee.calculator.preferred_flat_percent + -# %td= enterprise_fee.fee_type %br %div %a.button{ href: "#{main_app.admin_enterprise_fees_path}"} diff --git a/app/views/admin/enterprises/form/_shipping_methods.html.haml b/app/views/admin/enterprises/form/_shipping_methods.html.haml index 2d7376abc8..445b026c38 100644 --- a/app/views/admin/enterprises/form/_shipping_methods.html.haml +++ b/app/views/admin/enterprises/form/_shipping_methods.html.haml @@ -5,12 +5,12 @@ %th Name %th Applies? %th + %tbody - @shipping_methods.each do |shipping_method| - %tbody - %tr{ ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } - %td= shipping_method.name - %td= f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil - %td= link_to "Edit", edit_admin_shipping_method_path(shipping_method) + %tr{ ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } + %td= shipping_method.name + %td= f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil + %td= link_to "Edit", edit_admin_shipping_method_path(shipping_method) %br .row .six.columns.alpha From abdcdea1a244cbafd8a5348c6bf29d7bad46e019 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 28 Nov 2014 16:41:17 +1100 Subject: [PATCH 414/681] Adding payment methods partial to new enterprirse edit screen --- app/views/admin/enterprises/_form.html.haml | 4 +++ .../form/_payment_methods.html.haml | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 app/views/admin/enterprises/form/_payment_methods.html.haml diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 21789eb66e..aab758f0df 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -31,6 +31,10 @@ %legend Shipping Methods = render 'admin/enterprises/form/shipping_methods', f: f +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Payment Methods'" } } + %legend Payment Methods + = render 'admin/enterprises/form/payment_methods', f: f + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Enterprise Fees'" } } %legend Enterprise Fees = render 'admin/enterprises/form/enterprise_fees', f: f diff --git a/app/views/admin/enterprises/form/_payment_methods.html.haml b/app/views/admin/enterprises/form/_payment_methods.html.haml new file mode 100644 index 0000000000..8857980bd9 --- /dev/null +++ b/app/views/admin/enterprises/form/_payment_methods.html.haml @@ -0,0 +1,33 @@ +- if @payment_methods.count > 0 + %table + %thead + %tr + %th Name + %th Applies? + %th + %tbody + - @payment_methods.each do |payment_method| + %tr{ ng: { controller: 'paymentMethodCtrl', init: "findPaymentMethodByID(#{payment_method.id})" } } + %td= payment_method.name + %td= f.check_box :payment_method_ids, { multiple: true, 'ng-model' => 'PaymentMethod.selected' }, payment_method.id, nil + %td= link_to "Edit", edit_admin_payment_method_path(payment_method) + %br + .row + .six.columns.alpha + %a.button{ href: "#{admin_payment_methods_path}"} + Manage Payment Methods + %i.icon-arrow-right + .five.columns.omega.text-right + %a.button{ href: "#{new_admin_payment_method_path}"} + Create New Payment Method + %i.icon-plus + +- else + %p.text-center + You don't have any payment methods yet. + + %br + .text-center + %a.button{ href: "#{new_admin_payment_method_path}"} + Create One Now + %i.icon-arrow-right \ No newline at end of file From 612c8a95b27ff5ab28298ce8edccb04231d34f6c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 10 Dec 2014 10:54:30 +1100 Subject: [PATCH 415/681] Adding icons to enterprise edit side menu --- .../side_menu_controller.js.coffee | 22 +++++++++---------- .../admin/enterprises/_side_menu.html.haml | 3 ++- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index 590728d19f..3c3798ad2f 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -5,17 +5,17 @@ angular.module("admin.enterprises") $scope.select = SideMenu.select $scope.menu.setItems [ - { name: 'Primary Details' } - { name: 'Address' } - { name: 'Contact' } - { name: 'Social' } - { name: 'About' } - { name: 'Business Details' } - { name: 'Images' } - { name: "Shipping Methods" } - { name: "Payment Methods" } - { name: "Enterprise Fees" } - { name: "Preferences" } + { name: 'Primary Details', icon_class: "icon-user" } + { name: 'Address', icon_class: "icon-globe" } + { name: 'Contact', icon_class: "icon-phone" } + { name: 'Social', icon_class: "icon-twitter" } + { name: 'About', icon_class: "icon-pencil" } + { name: 'Business Details', icon_class: "icon-barcode" } + { name: 'Images', icon_class: "icon-picture" } + { name: "Shipping Methods", icon_class: "icon-truck" } + { name: "Payment Methods", icon_class: "icon-money" } + { name: "Enterprise Fees", icon_class: "icon-tasks" } + { name: "Preferences", icon_class: "icon-cog" } ] $scope.select(0) diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/enterprises/_side_menu.html.haml index 687c2d2a12..ee23e41d73 100644 --- a/app/views/admin/enterprises/_side_menu.html.haml +++ b/app/views/admin/enterprises/_side_menu.html.haml @@ -5,4 +5,5 @@ class: '{ selected: item.selected}', 'class-odd' => "'odd'", 'class-even' => "'even'" } } - {{ item.name }} \ No newline at end of file + %i{ class: "{{item.icon_class}}" } + %span {{ item.name }} \ No newline at end of file From 33d4f03a99f540f457bb8cb456617932bd26dd6c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 10 Dec 2014 11:10:30 +1100 Subject: [PATCH 416/681] Adding shop preferences pane, fiddling with icons --- .../enterprises/controllers/side_menu_controller.js.coffee | 6 +++--- app/views/admin/enterprises/_form.html.haml | 4 ++++ .../admin/enterprises/form/_shop_preferences.html.haml | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 app/views/admin/enterprises/form/_shop_preferences.html.haml diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index 3c3798ad2f..b893373b98 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -6,16 +6,16 @@ angular.module("admin.enterprises") $scope.menu.setItems [ { name: 'Primary Details', icon_class: "icon-user" } - { name: 'Address', icon_class: "icon-globe" } + { name: 'Address', icon_class: "icon-map-marker" } { name: 'Contact', icon_class: "icon-phone" } { name: 'Social', icon_class: "icon-twitter" } { name: 'About', icon_class: "icon-pencil" } - { name: 'Business Details', icon_class: "icon-barcode" } + { name: 'Business Details', icon_class: "icon-briefcase" } { name: 'Images', icon_class: "icon-picture" } { name: "Shipping Methods", icon_class: "icon-truck" } { name: "Payment Methods", icon_class: "icon-money" } { name: "Enterprise Fees", icon_class: "icon-tasks" } - { name: "Preferences", icon_class: "icon-cog" } + { name: "Shop Preferences", icon_class: "icon-shopping-cart" } ] $scope.select(0) diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index aab758f0df..d63ae9c143 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -38,3 +38,7 @@ %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Enterprise Fees'" } } %legend Enterprise Fees = render 'admin/enterprises/form/enterprise_fees', f: f + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Shop Preferences'" } } + %legend Shop Preferences + = render 'admin/enterprises/form/shop_preferences', f: f diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml new file mode 100644 index 0000000000..cbf11fc474 --- /dev/null +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -0,0 +1 @@ +Something \ No newline at end of file From 88d49148d45758e256d87c4cc753ba88e2c14812 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 17 Dec 2014 12:10:08 +1100 Subject: [PATCH 417/681] Adding border to text-angular form element --- app/assets/stylesheets/admin/openfoodnetwork.css.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 62376cb052..641e6c5756 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -11,6 +11,11 @@ table .blank-action { width: 29px; } +text-angular .ta-editor { + border: 1px solid #cee1f4; + border-radius: 3px; +} + #header #logo { top: 10px; } From a489aa7ad9af5e819409a08020a7dfa99760df6f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 17 Dec 2014 12:45:41 +1100 Subject: [PATCH 418/681] Fixing enterprise feature spec, adding first shop preference --- .../enterprise_controller.js.coffee | 16 ++++++- .../stylesheets/admin/side_menu.css.sass | 1 + app/models/enterprise.rb | 2 + .../admin/enterprises/_side_menu.html.haml | 3 +- .../form/_shop_preferences.html.haml | 10 +++- spec/features/admin/enterprises_spec.rb | 47 +++++++++++++++++-- 6 files changed, 70 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 9a665571b2..3c32c862c1 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -47,9 +47,21 @@ angular.module("admin.enterprises") , 0 $scope.$watch "Enterprise.is_primary_producer", (newValue, oldValue) -> - if newValue + if !newValue && $scope.Enterprise.sells == "none" + $scope.menu.hide_item_by_name('Enterprise Fees') + else + $scope.menu.show_item_by_name('Enterprise Fees') + + + $scope.$watch "Enterprise.sells", (newValue, oldValue) -> + if newValue == "none" $scope.menu.hide_item_by_name('Shipping Methods') $scope.menu.hide_item_by_name('Payment Methods') - else if !newValue + if $scope.Enterprise.is_primary_producer + $scope.menu.show_item_by_name('Enterprise Fees') + else + $scope.menu.hide_item_by_name('Enterprise Fees') + else $scope.menu.show_item_by_name('Shipping Methods') $scope.menu.show_item_by_name('Payment Methods') + $scope.menu.show_item_by_name('Enterprise Fees') diff --git a/app/assets/stylesheets/admin/side_menu.css.sass b/app/assets/stylesheets/admin/side_menu.css.sass index a41e60f711..6c03059d27 100644 --- a/app/assets/stylesheets/admin/side_menu.css.sass +++ b/app/assets/stylesheets/admin/side_menu.css.sass @@ -2,6 +2,7 @@ border-right: 2px solid #f6f6f6 border-top: 2px solid #f6f6f6 .menu_item + display: block padding: 8px 15px font-size: 120% cursor: pointer diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 5082267a01..49c2dd29e6 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -3,6 +3,8 @@ class Enterprise < ActiveRecord::Base SHOP_TRIAL_LENGTH = 30 ENTERPRISE_SEARCH_RADIUS = 100 + preference :shopfront_message, :text, default: "" + devise :confirmable, reconfirmable: true, confirmation_keys: [ :id, :email ] self.inheritance_column = nil diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/enterprises/_side_menu.html.haml index ee23e41d73..a1d32d3ecc 100644 --- a/app/views/admin/enterprises/_side_menu.html.haml +++ b/app/views/admin/enterprises/_side_menu.html.haml @@ -1,5 +1,6 @@ .side_menu{ ng: { controller: 'sideMenuCtrl' } } - .menu_item{ ng: { repeat: '(index,item) in menu.items', + %a.menu_item{ href: "#", id: "{{ item.name.toLowerCase().replace(' ', '_') }}", + ng: { repeat: '(index,item) in menu.items', show: "item.visible", click: 'select(index)', class: '{ selected: item.selected}', diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index cbf11fc474..a3c4cc3379 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -1 +1,9 @@ -Something \ No newline at end of file +.row + .alpha.eleven.columns + .three.columns.alpha + = f.label "preferred_shopfront_message", t(:shopfront_message) + .eight.columns.omega + -# %text-angular{'ng-model' => 'longDescription', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', + -# 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", + -# 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} + = f.text_area :preferred_shopfront_message, rows: 6, class: "fullwidth", placeholder: "An option explanation for customers detailing how your shopfront works. This text will appear at the top of your shop page, immediately above your products." \ No newline at end of file diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 3383717460..2d88480b31 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -131,24 +131,37 @@ feature %q{ select2_search admin.email, from: 'Owner' select2_search admin.email, from: 'Owner' choose 'Any' - check "enterprise_payment_method_ids_#{payment_method.id}" - check "enterprise_shipping_method_ids_#{shipping_method.id}" select2_search eg1.name, from: 'Groups' + + click_link "Payment Methods" + check "enterprise_payment_method_ids_#{payment_method.id}" + + click_link "Shipping Methods" + check "enterprise_shipping_method_ids_#{shipping_method.id}" + + click_link "Contact" fill_in 'enterprise_contact', :with => 'Kirsten or Ren' fill_in 'enterprise_phone', :with => '0413 897 321' fill_in 'enterprise_email', :with => 'info@eaterprises.com.au' fill_in 'enterprise_website', :with => 'http://eaterprises.com.au' + + click_link "Social" fill_in 'enterprise_twitter', :with => '@eaterprises' fill_in 'enterprise_facebook', :with => 'facebook.com/eaterprises' fill_in 'enterprise_instagram', :with => 'eaterprises' + + click_link "Business Details" fill_in 'enterprise_abn', :with => '09812309823' fill_in 'enterprise_acn', :with => '' + click_link "Address" fill_in 'enterprise_address_attributes_address1', :with => '35 Ballantyne St' fill_in 'enterprise_address_attributes_city', :with => 'Thornbury' fill_in 'enterprise_address_attributes_zipcode', :with => '3072' select2_search 'Australia', :from => 'Country' select2_search 'Victoria', :from => 'State' + + click_link "About" long_description = find :css, "text-angular div.ta-scroll-window div.ta-bind" long_description.set 'Connecting farmers and eaters' @@ -176,11 +189,14 @@ feature %q{ fill_in 'enterprise_name', :with => 'Eaterprises' choose 'Own' select2_search user.email, from: 'Owner' + + click_link "About" fill_in 'enterprise_description', :with => 'Connecting farmers and eaters' long_description = find :css, "text-angular div.ta-scroll-window div.ta-bind" long_description.set 'This is an interesting long description' # Check Angularjs switching of sidebar elements + click_link "Primary Details" uncheck 'enterprise_is_primary_producer' choose 'None' page.should have_selector "#enterprise_fees", visible: false @@ -202,26 +218,37 @@ feature %q{ select2_search eg1.name, from: 'Groups' + click_link "Payment Methods" page.should_not have_checked_field "enterprise_payment_method_ids_#{payment_method.id}" - page.should_not have_checked_field "enterprise_shipping_method_ids_#{shipping_method.id}" - check "enterprise_payment_method_ids_#{payment_method.id}" + + click_link "Shipping Methods" + page.should_not have_checked_field "enterprise_shipping_method_ids_#{shipping_method.id}" check "enterprise_shipping_method_ids_#{shipping_method.id}" + click_link "Contact" fill_in 'enterprise_contact', :with => 'Kirsten or Ren' fill_in 'enterprise_phone', :with => '0413 897 321' fill_in 'enterprise_email', :with => 'info@eaterprises.com.au' fill_in 'enterprise_website', :with => 'http://eaterprises.com.au' + + click_link "Social" fill_in 'enterprise_twitter', :with => '@eaterprises' + + click_link "Business Details" fill_in 'enterprise_abn', :with => '09812309823' fill_in 'enterprise_acn', :with => '' + click_link "Address" fill_in 'enterprise_address_attributes_address1', :with => '35 Ballantyne St' fill_in 'enterprise_address_attributes_city', :with => 'Thornbury' fill_in 'enterprise_address_attributes_zipcode', :with => '3072' select2_search 'Australia', :from => 'Country' select2_search 'Victoria', :from => 'State' + click_link "Shop Preferences" + fill_in 'enterprise_preferred_shopfront_message', :with => 'This is my shopfront message.' + click_button 'Update' flash_message.should == 'Enterprise "Eaterprises" has been successfully updated!' @@ -229,10 +256,20 @@ feature %q{ @enterprise.reload expect(@enterprise.owner).to eq user + click_link "Payment Methods" page.should have_checked_field "enterprise_payment_method_ids_#{payment_method.id}" + + click_link "Shipping Methods" page.should have_checked_field "enterprise_shipping_method_ids_#{shipping_method.id}" - page.should have_selector "a.list-item", text: enterprise_fee.name + + click_link "Enterprise Fees" + page.should have_selector "td", text: enterprise_fee.name + + click_link "About" page.should have_content 'This is an interesting long description' + + click_link "Shop Preferences" + page.should have_field 'enterprise_preferred_shopfront_message', :text => 'This is my shopfront message.' end describe "producer properties" do From 435bc177f89c6abd97fa593ffb06153215d24d45 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 17 Dec 2014 12:58:00 +1100 Subject: [PATCH 419/681] Hiding side menu elements by filtering rather than by making invisible --- app/views/admin/enterprises/_side_menu.html.haml | 3 +-- spec/features/admin/enterprises_spec.rb | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/enterprises/_side_menu.html.haml index a1d32d3ecc..34919ca169 100644 --- a/app/views/admin/enterprises/_side_menu.html.haml +++ b/app/views/admin/enterprises/_side_menu.html.haml @@ -1,7 +1,6 @@ .side_menu{ ng: { controller: 'sideMenuCtrl' } } %a.menu_item{ href: "#", id: "{{ item.name.toLowerCase().replace(' ', '_') }}", - ng: { repeat: '(index,item) in menu.items', - show: "item.visible", + ng: { repeat: '(index,item) in menu.items | filter:{visible:true}', click: 'select(index)', class: '{ selected: item.selected}', 'class-odd' => "'odd'", diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 2d88480b31..f89b800f07 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -199,13 +199,13 @@ feature %q{ click_link "Primary Details" uncheck 'enterprise_is_primary_producer' choose 'None' - page.should have_selector "#enterprise_fees", visible: false - page.should have_selector "#payment_methods", visible: false - page.should have_selector "#shipping_methods", visible: false + page.should_not have_selector "#enterprise_fees" + page.should_not have_selector "#payment_methods" + page.should_not have_selector "#shipping_methods" check 'enterprise_is_primary_producer' page.should have_selector "#enterprise_fees" - page.should have_selector "#payment_methods", visible: false - page.should have_selector "#shipping_methods", visible: false + page.should_not have_selector "#payment_methods" + page.should_not have_selector "#shipping_methods" uncheck 'enterprise_is_primary_producer' choose 'Own' page.should have_selector "#enterprise_fees" From 5a24f2c0e2364f30b96a5f23dcff0f7cf987b1e5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 17 Dec 2014 14:35:11 +1100 Subject: [PATCH 420/681] WIP: adding shopfront message to shop --- app/assets/stylesheets/darkswarm/shop.css.sass | 11 +++++++++++ app/views/shop/_messages.html.haml | 10 ++++++++++ app/views/shop/show.html.haml | 8 +++++--- 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 app/views/shop/_messages.html.haml diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 554d3f2f4e..ca349efd44 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -68,3 +68,14 @@ i.ofn-i_036-producers padding-left: 0.5rem + + .shopfront_message, .shopfront_closed_message + padding: 15px + border-radius: 5px + border: 2px solid #eb4c46 + + .shopfront_message + margin-top: 2em + + .shopfront_closed_message + margin: 2em 0em diff --git a/app/views/shop/_messages.html.haml b/app/views/shop/_messages.html.haml new file mode 100644 index 0000000000..7a0a5af811 --- /dev/null +++ b/app/views/shop/_messages.html.haml @@ -0,0 +1,10 @@ +- if @order_cycles and @order_cycles.empty? + .row + .small-12.columns + .shopfront_closed_message + = current_distributor.preferred_shopfront_message +- elsif current_distributor.preferred_shopfront_message.present? + .row + .small-12.columns + .shopfront_message + = current_distributor.preferred_shopfront_message diff --git a/app/views/shop/show.html.haml b/app/views/shop/show.html.haml index 048dd13e73..89ce1a30ac 100644 --- a/app/views/shop/show.html.haml +++ b/app/views/shop/show.html.haml @@ -1,4 +1,4 @@ -= inject_enterprises += inject_enterprises %shop.darkswarm - content_for :order_cycle_form do @@ -12,15 +12,17 @@ / Will this label should be a variable to reflect 'Ready for pickup / delivery' as appropriate - %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", + %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", "ng-change" => "changeOrderCycle()", "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", "popover-placement" => "left", "popover" => "Choose when you want your order:", "popover-trigger" => "openTrigger"} - + = render partial: "shopping_shared/details" + = render partial: 'shop/messages' + .row = render partial: "shop/products/form" From d5df73c6a25183498d432d78bd0dcd1dce970f60 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 17 Dec 2014 15:17:29 +1100 Subject: [PATCH 421/681] Duplicate create basic version of enterprise serializer for lists, make shopfront_message input use textangular --- app/helpers/admin/injection_helper.rb | 4 ++-- app/serializers/api/admin/basic_enterprise_serializer.rb | 4 ++++ app/serializers/api/admin/enterprise_serializer.rb | 4 ++-- .../admin/enterprises/form/_shop_preferences.html.haml | 7 +++---- app/views/shop/_messages.html.haml | 4 ++-- 5 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 app/serializers/api/admin/basic_enterprise_serializer.rb diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index f01f9b9b01..ec03378ff0 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -5,8 +5,8 @@ module Admin end def admin_inject_enterprises - admin_inject_json_ams_array("ofn.admin", "my_enterprises", @my_enterprises, Api::Admin::EnterpriseSerializer) + - admin_inject_json_ams_array("ofn.admin", "all_enterprises", @all_enterprises, Api::Admin::EnterpriseSerializer) + admin_inject_json_ams_array("ofn.admin", "my_enterprises", @my_enterprises, Api::Admin::BasicEnterpriseSerializer) + + admin_inject_json_ams_array("ofn.admin", "all_enterprises", @all_enterprises, Api::Admin::BasicEnterpriseSerializer) end def admin_inject_enterprise_relationships diff --git a/app/serializers/api/admin/basic_enterprise_serializer.rb b/app/serializers/api/admin/basic_enterprise_serializer.rb new file mode 100644 index 0000000000..f060dc4166 --- /dev/null +++ b/app/serializers/api/admin/basic_enterprise_serializer.rb @@ -0,0 +1,4 @@ +class Api::Admin::BasicEnterpriseSerializer < ActiveModel::Serializer + attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids + attributes :producer_profile_only +end diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 37c7e9af27..174c5c4daa 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -1,4 +1,4 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids - attributes :producer_profile_only, :email -end + attributes :producer_profile_only, :email, :long_description, :preferred_shopfront_message +end \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index a3c4cc3379..2d23f2f335 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -3,7 +3,6 @@ .three.columns.alpha = f.label "preferred_shopfront_message", t(:shopfront_message) .eight.columns.omega - -# %text-angular{'ng-model' => 'longDescription', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', - -# 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", - -# 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} - = f.text_area :preferred_shopfront_message, rows: 6, class: "fullwidth", placeholder: "An option explanation for customers detailing how your shopfront works. This text will appear at the top of your shop page, immediately above your products." \ No newline at end of file + %text-angular{'ng-model' => 'Enterprise.preferred_shopfront_message', 'id' => 'enterprise_preferred_shopfront_message', 'name' => 'enterprise[preferred_shopfront_message]', 'class' => 'text-angular', + 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", + 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} diff --git a/app/views/shop/_messages.html.haml b/app/views/shop/_messages.html.haml index 7a0a5af811..2cff1b30b5 100644 --- a/app/views/shop/_messages.html.haml +++ b/app/views/shop/_messages.html.haml @@ -2,9 +2,9 @@ .row .small-12.columns .shopfront_closed_message - = current_distributor.preferred_shopfront_message + = current_distributor.preferred_shopfront_message.html_safe - elsif current_distributor.preferred_shopfront_message.present? .row .small-12.columns .shopfront_message - = current_distributor.preferred_shopfront_message + = current_distributor.preferred_shopfront_message.html_safe From 3fa1832e7b532ba810a3af79f47026562933b012 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 17 Dec 2014 15:39:32 +1100 Subject: [PATCH 422/681] Adding shop closed message preference --- app/models/enterprise.rb | 1 + app/serializers/api/admin/enterprise_serializer.rb | 2 +- .../admin/enterprises/form/_shop_preferences.html.haml | 10 +++++++++- app/views/shop/_messages.html.haml | 9 +++++---- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 49c2dd29e6..0ab440f0a7 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -4,6 +4,7 @@ class Enterprise < ActiveRecord::Base ENTERPRISE_SEARCH_RADIUS = 100 preference :shopfront_message, :text, default: "" + preference :shopfront_closed_message, :text, default: "" devise :confirmable, reconfirmable: true, confirmation_keys: [ :id, :email ] diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 174c5c4daa..d63e2d1537 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -1,4 +1,4 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids - attributes :producer_profile_only, :email, :long_description, :preferred_shopfront_message + attributes :producer_profile_only, :email, :long_description, :preferred_shopfront_message, :preferred_shopfront_closed_message end \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index 2d23f2f335..bda68ec0ae 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -5,4 +5,12 @@ .eight.columns.omega %text-angular{'ng-model' => 'Enterprise.preferred_shopfront_message', 'id' => 'enterprise_preferred_shopfront_message', 'name' => 'enterprise[preferred_shopfront_message]', 'class' => 'text-angular', 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", - 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} + 'placeholder' => 'An optional explanation for customers detailing how your shopfront works, to be displayed above the product list on your shop page.'} +.row + .alpha.eleven.columns + .three.columns.alpha + = f.label "preferred_shopfront_closed_message", t(:shopfront_closed_message) + .eight.columns.omega + %text-angular{'ng-model' => 'Enterprise.preferred_shopfront_closed_message', 'id' => 'enterprise_preferred_shopfront_closed_message', 'name' => 'enterprise[preferred_shopfront_closed_message]', 'class' => 'text-angular', + 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", + 'placeholder' => 'A message which provides a more detailed explanation about why your shop is closed and/or when customers can expect it to open again. This is displayed on your shop only when you have no active order cycles (ie. shop is closed).'} diff --git a/app/views/shop/_messages.html.haml b/app/views/shop/_messages.html.haml index 2cff1b30b5..1cca55bb35 100644 --- a/app/views/shop/_messages.html.haml +++ b/app/views/shop/_messages.html.haml @@ -1,8 +1,9 @@ - if @order_cycles and @order_cycles.empty? - .row - .small-12.columns - .shopfront_closed_message - = current_distributor.preferred_shopfront_message.html_safe + - if current_distributor.preferred_shopfront_closed_message.present? + .row + .small-12.columns + .shopfront_closed_message + = current_distributor.preferred_shopfront_closed_message.html_safe - elsif current_distributor.preferred_shopfront_message.present? .row .small-12.columns From b18163bfaf109e5d752992e1d390a3c2983d4251 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 17 Dec 2014 16:02:06 +1100 Subject: [PATCH 423/681] Remove separate long description injection, use full enterprise serializer instead --- .../controllers/enterprise_controller.js.coffee | 3 +-- app/helpers/admin/injection_helper.rb | 7 ------- app/views/admin/enterprises/_form_data.html.haml | 1 - app/views/admin/enterprises/form/_about_us.html.haml | 2 +- .../controllers/enterprise_controller_spec.js.coffee | 10 ++++------ 5 files changed, 6 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 3c32c862c1..ef175c96db 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,11 +1,10 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, longDescription, NavigationCheck, Enterprise, PaymentMethods, ShippingMethods, SideMenu) -> + .controller "enterpriseCtrl", ($scope, NavigationCheck, Enterprise, PaymentMethods, ShippingMethods, SideMenu) -> $scope.Enterprise = Enterprise.enterprise $scope.PaymentMethods = PaymentMethods.paymentMethods $scope.ShippingMethods = ShippingMethods.shippingMethods $scope.navClear = NavigationCheck.clear # htmlVariable is used by textAngular wysiwyg for the long descrtiption. - $scope.htmlVariable = longDescription $scope.pristineEmail = $scope.Enterprise.email $scope.menu = SideMenu diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index ec03378ff0..330fc80ed9 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -57,13 +57,6 @@ module Admin render partial: "admin/json/injection_ams", locals: {ngModule: 'ofn.admin', name: 'SpreeApiKey', json: "'#{@spree_api_key.to_s}'"} end - def admin_inject_enterprise_long_description - # Clean line breaks and quotes. - long_description = @enterprise.long_description.blank? ? "" : @enterprise.long_description.gsub("\r\n", "
    ").gsub("\"", """).gsub("'","'") - render partial: "admin/json/injection_ams", locals: {ngModule: 'admin.enterprises', name: 'longDescription', json: "'#{long_description}'"} - end - - def admin_inject_json_ams(ngModule, name, data, serializer, opts = {}) json = serializer.new(data, scope: spree_current_user).to_json render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} diff --git a/app/views/admin/enterprises/_form_data.html.haml b/app/views/admin/enterprises/_form_data.html.haml index a7d34a1aad..90f2148c09 100644 --- a/app/views/admin/enterprises/_form_data.html.haml +++ b/app/views/admin/enterprises/_form_data.html.haml @@ -1,4 +1,3 @@ = admin_inject_enterprise -= admin_inject_enterprise_long_description = admin_inject_payment_methods = admin_inject_shipping_methods \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_about_us.html.haml b/app/views/admin/enterprises/form/_about_us.html.haml index e00be48828..a04437dad0 100644 --- a/app/views/admin/enterprises/form/_about_us.html.haml +++ b/app/views/admin/enterprises/form/_about_us.html.haml @@ -12,6 +12,6 @@ -# ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'], -# ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], -# ['html', 'insertImage', 'insertLink', 'insertVideo'] - %text-angular{'ng-model' => 'htmlVariable', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', + %text-angular{'ng-model' => 'Enterprise.long_description', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", 'placeholder' => 'Tell customers about yourself. This information appears on your public profile.'} \ No newline at end of file diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee index f6805fd04d..4018ff80bc 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee @@ -4,24 +4,22 @@ describe "enterpriseCtrl", -> Enterprise = null PaymentMethods = null ShippingMethods = null - longDescriptionMock = ["long description text"] beforeEach -> module('admin.enterprises') - module ($provide)-> - $provide.value "longDescription", longDescriptionMock - null Enterprise = enterprise: payment_method_ids: [ 1, 3 ] shipping_method_ids: [ 2, 4 ] + is_primary_producer: true + sells: "none" PaymentMethods = paymentMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] ShippingMethods = shippingMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] - inject ($controller) -> - scope = {} + inject ($rootScope, $controller) -> + scope = $rootScope ctrl = $controller 'enterpriseCtrl', {$scope: scope, Enterprise: Enterprise, PaymentMethods: PaymentMethods, ShippingMethods: ShippingMethods} describe "initialisation", -> From c5bf228cb9886579cbe7f8cef395758a1b0e6970 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 18 Dec 2014 10:23:44 +1100 Subject: [PATCH 424/681] Removing hashes from side menu links --- app/views/admin/enterprises/_side_menu.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/enterprises/_side_menu.html.haml index 34919ca169..383d5181fb 100644 --- a/app/views/admin/enterprises/_side_menu.html.haml +++ b/app/views/admin/enterprises/_side_menu.html.haml @@ -1,5 +1,5 @@ .side_menu{ ng: { controller: 'sideMenuCtrl' } } - %a.menu_item{ href: "#", id: "{{ item.name.toLowerCase().replace(' ', '_') }}", + %a.menu_item{ href: "", id: "{{ item.name.toLowerCase().replace(' ', '_') }}", ng: { repeat: '(index,item) in menu.items | filter:{visible:true}', click: 'select(index)', class: '{ selected: item.selected}', From 2efd9052275c817d225f0d41eb9f1168ba2631a7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 18 Dec 2014 10:24:00 +1100 Subject: [PATCH 425/681] Fixing specs to work with text-angular --- spec/features/admin/enterprises_spec.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index f89b800f07..4a751c4aab 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -192,7 +192,7 @@ feature %q{ click_link "About" fill_in 'enterprise_description', :with => 'Connecting farmers and eaters' - long_description = find :css, "text-angular div.ta-scroll-window div.ta-bind" + long_description = find :css, "text-angular#enterprise_long_description div.ta-scroll-window div.ta-bind" long_description.set 'This is an interesting long description' # Check Angularjs switching of sidebar elements @@ -247,7 +247,8 @@ feature %q{ select2_search 'Victoria', :from => 'State' click_link "Shop Preferences" - fill_in 'enterprise_preferred_shopfront_message', :with => 'This is my shopfront message.' + shopfront_message = find :css, "text-angular#enterprise_preferred_shopfront_message div.ta-scroll-window div.ta-bind" + shopfront_message.set 'This is my shopfront message.' click_button 'Update' @@ -269,7 +270,7 @@ feature %q{ page.should have_content 'This is an interesting long description' click_link "Shop Preferences" - page.should have_field 'enterprise_preferred_shopfront_message', :text => 'This is my shopfront message.' + page.should have_content 'This is my shopfront message.' end describe "producer properties" do From 9f086facdb267d68dd38f4672f2c6f1494e5bbaf Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 18 Dec 2014 10:28:15 +1100 Subject: [PATCH 426/681] Hiding link to shopfront for non-distributors --- .../enterprises/form/_primary_details.html.haml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml index 91d8661518..e25060fb14 100644 --- a/app/views/admin/enterprises/form/_primary_details.html.haml +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -66,12 +66,10 @@ = f.radio_button :visible, false   = f.label :visible, "Not Visible", :value => "false" -- if @enterprise.is_distributor -- # TODO: Angularise this - .row - .three.columns.alpha - %label Link to shop front - .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} - %a What's this? - .eight.columns.omega - = main_app.shop_enterprise_url(@enterprise) \ No newline at end of file +.row{ ng: { show: "Enterprise.sells == 'own' || Enterprise.sells == 'any'" } } + .three.columns.alpha + %label Link to shop front + .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} + %a What's this? + .eight.columns.omega + = main_app.shop_enterprise_url(@enterprise) \ No newline at end of file From 4b353fa27b023e824873bbb2d6bd760e939d24ec Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 18 Dec 2014 11:01:08 +1100 Subject: [PATCH 427/681] Ammending comments to make slightly more clear --- app/models/order_cycle.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index d5a1371876..dcf96633c4 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -113,8 +113,8 @@ class OrderCycle < ActiveRecord::Base end # If a product without variants is added to an order cycle, and then some variants are added - # to that product, then the master variant is still part of the order cycle, but customers - # should not be able to purchase it. + # to that product, but not the order cycle, then the master variant should not available for customers + # to purchase. # This method filters out such products so that the customer cannot purchase them. def valid_products_distributed_by(distributor) variants = variants_distributed_by(distributor) @@ -175,8 +175,8 @@ class OrderCycle < ActiveRecord::Base private # If a product without variants is added to an order cycle, and then some variants are added - # to that product, then the master variant is still part of the order cycle, but customers - # should not be able to purchase it. + # to that product, but not the order cycle, then the master variant should not available for customers + # to purchase. # This method is used by #valid_products_distributed_by to filter out such products so that # the customer cannot purchase them. def product_has_only_obsolete_master_in_distribution?(product, distributed_variants) From a38e27e504e2158127e8e82f6d2277aeed35c1ab Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 18 Dec 2014 15:47:20 +1100 Subject: [PATCH 428/681] Adapting taxon autocompleter to allow multiple selection of taxons --- .../directives/taxon_autocomplete.js.coffee | 21 +++++++++++-------- .../bulk_edit/_products_product.html.haml | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee index 6830bd7d22..9c5197121a 100644 --- a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee @@ -1,19 +1,22 @@ angular.module("ofn.admin").directive "ofnTaxonAutocomplete", (Taxons) -> # Adapted from Spree's existing taxon autocompletion - require: "ngModel" - link: (scope,element,attrs,ngModel) -> + scope: true + link: (scope,element,attrs) -> + multiple = scope.$eval attrs.multipleSelection + placeholder = attrs.placeholder + setTimeout -> element.select2 - placeholder: "Category" - multiple: false + placeholder: placeholder + multiple: multiple initSelection: (element, callback) -> - callback Taxons.findByID(scope.product.category_id) + if multiple + callback Taxons.findByIDs(scope.product.category_id) + else + callback Taxons.findByID(scope.product.category_id) query: (query) -> query.callback { results: Taxons.findByTerm(query.term) } formatResult: (taxon) -> taxon.name formatSelection: (taxon) -> - taxon.name - element.on "change", -> - scope.$apply -> - ngModel.$setViewValue element.val() \ No newline at end of file + taxon.name \ No newline at end of file diff --git a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml index ed41532a11..bf527ba507 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml @@ -19,7 +19,7 @@ %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.category{ 'ng-if' => 'columns.category.visible' } - %input.fullwidth{ :type => 'text', id: "p{{product.id}}_category_id", 'ng-model' => 'product.category_id', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category_id' } + %input.fullwidth{ :type => 'text', id: "p{{product.id}}_category_id", 'ng-model' => 'product.category_id', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category_id', 'multiple-selection' => 'false', placeholder: 'Category' } %td.available_on{ 'ng-show' => 'columns.available_on.visible' } %input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ofn-track-product' => 'available_on', 'datetimepicker' => 'product.available_on', type: "text" } %td.actions From f145a7ed65d2da0f332e68115b7f899e5cad4ba2 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 18 Dec 2014 16:50:47 +1100 Subject: [PATCH 429/681] Moving taxons into its own module, adding preferred shopfront taxon order to enterprise --- app/assets/javascripts/admin/admin.js.coffee | 2 +- app/assets/javascripts/admin/all.js | 1 + .../admin/enterprises/enterprises.js.coffee | 2 +- .../directives/taxon_autocomplete.js.coffee | 7 ++++--- .../admin/{ => taxons}/services/taxons.js.coffee | 2 +- .../javascripts/admin/taxons/taxons.js.coffee | 1 + app/controllers/admin/enterprises_controller.rb | 5 +++++ app/helpers/admin/injection_helper.rb | 2 +- app/models/enterprise.rb | 5 +++-- app/serializers/api/admin/enterprise_serializer.rb | 3 ++- app/views/admin/enterprises/_form_data.html.haml | 1 + .../enterprises/form/_shop_preferences.html.haml | 13 +++++++++++-- 12 files changed, 32 insertions(+), 12 deletions(-) rename app/assets/javascripts/admin/{ => taxons}/directives/taxon_autocomplete.js.coffee (68%) rename app/assets/javascripts/admin/{ => taxons}/services/taxons.js.coffee (77%) create mode 100644 app/assets/javascripts/admin/taxons/taxons.js.coffee diff --git a/app/assets/javascripts/admin/admin.js.coffee b/app/assets/javascripts/admin/admin.js.coffee index cbb824f97d..1c9f65f91a 100644 --- a/app/assets/javascripts/admin/admin.js.coffee +++ b/app/assets/javascripts/admin/admin.js.coffee @@ -1,3 +1,3 @@ -angular.module("ofn.admin", ["ngResource", "ngAnimate", "ofn.dropdown", "admin.products", "infinite-scroll"]).config ($httpProvider) -> +angular.module("ofn.admin", ["ngResource", "ngAnimate", "ofn.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content") $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index d51898775d..78ff8306f8 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -23,6 +23,7 @@ //= require ./products/products //= require ./shipping_methods/shipping_methods //= require ./side_menu/side_menu +//= require ./taxons/taxons //= require ./utils/utils //= require ./users/users //= require textAngular.min.js diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index 1568af32f7..5a7942d639 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu"] ) \ No newline at end of file +angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons"] ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee b/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee similarity index 68% rename from app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee rename to app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee index 9c5197121a..aac40abd46 100644 --- a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee @@ -1,9 +1,10 @@ -angular.module("ofn.admin").directive "ofnTaxonAutocomplete", (Taxons) -> +angular.module("admin.taxons").directive "ofnTaxonAutocomplete", (Taxons) -> # Adapted from Spree's existing taxon autocompletion scope: true link: (scope,element,attrs) -> multiple = scope.$eval attrs.multipleSelection placeholder = attrs.placeholder + initalSelection = scope.$eval attrs.ngModel setTimeout -> element.select2 @@ -11,9 +12,9 @@ angular.module("ofn.admin").directive "ofnTaxonAutocomplete", (Taxons) -> multiple: multiple initSelection: (element, callback) -> if multiple - callback Taxons.findByIDs(scope.product.category_id) + callback Taxons.findByIDs(initalSelection) else - callback Taxons.findByID(scope.product.category_id) + callback Taxons.findByID(initalSelection) query: (query) -> query.callback { results: Taxons.findByTerm(query.term) } formatResult: (taxon) -> diff --git a/app/assets/javascripts/admin/services/taxons.js.coffee b/app/assets/javascripts/admin/taxons/services/taxons.js.coffee similarity index 77% rename from app/assets/javascripts/admin/services/taxons.js.coffee rename to app/assets/javascripts/admin/taxons/services/taxons.js.coffee index 6944fe132f..9257b637e1 100644 --- a/app/assets/javascripts/admin/services/taxons.js.coffee +++ b/app/assets/javascripts/admin/taxons/services/taxons.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").factory "Taxons", (taxons, $filter) -> +angular.module("admin.taxons").factory "Taxons", (taxons, $filter) -> new class Taxons constructor: -> @taxons = taxons diff --git a/app/assets/javascripts/admin/taxons/taxons.js.coffee b/app/assets/javascripts/admin/taxons/taxons.js.coffee new file mode 100644 index 0000000000..863e6e8125 --- /dev/null +++ b/app/assets/javascripts/admin/taxons/taxons.js.coffee @@ -0,0 +1 @@ +angular.module("admin.taxons", []) \ No newline at end of file diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index c216b1badf..0ee0541145 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -3,6 +3,7 @@ module Admin before_filter :load_enterprise_set, :only => :index before_filter :load_countries, :except => :index before_filter :load_methods_and_fees, :only => [:new, :edit, :update, :create] + before_filter :load_taxons, :only => [:new, :edit, :update, :create] before_filter :check_can_change_sells, only: :update before_filter :check_can_change_bulk_sells, only: :bulk_update before_filter :override_owner, only: :create @@ -89,6 +90,10 @@ module Admin @enterprise_fees = EnterpriseFee.managed_by(spree_current_user).for_enterprise(@enterprise).order(:fee_type, :name).all end + def load_taxons + @taxons = Spree::Taxon.order(:name) + end + def check_can_change_bulk_sells unless spree_current_user.admin? params[:enterprise_set][:collection_attributes].each do |i, enterprise_params| diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 330fc80ed9..b35e2ab437 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -42,7 +42,7 @@ module Admin end def admin_inject_taxons - admin_inject_json_ams_array "ofn.admin", "taxons", @taxons, Api::Admin::TaxonSerializer + admin_inject_json_ams_array "admin.taxons", "taxons", @taxons, Api::Admin::TaxonSerializer end def admin_inject_users diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 0ab440f0a7..51c12fa011 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -3,8 +3,9 @@ class Enterprise < ActiveRecord::Base SHOP_TRIAL_LENGTH = 30 ENTERPRISE_SEARCH_RADIUS = 100 - preference :shopfront_message, :text, default: "" - preference :shopfront_closed_message, :text, default: "" + preference :shopfront_message, :text, default: nil + preference :shopfront_closed_message, :text, default: nil + preference :shopfront_taxon_order, :string, default: nil devise :confirmable, reconfirmable: true, confirmation_keys: [ :id, :email ] diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index d63e2d1537..14fdd5c8b6 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -1,4 +1,5 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids - attributes :producer_profile_only, :email, :long_description, :preferred_shopfront_message, :preferred_shopfront_closed_message + attributes :producer_profile_only, :email, :long_description + attributes :preferred_shopfront_message, :preferred_shopfront_closed_message, :preferred_shopfront_taxon_order end \ No newline at end of file diff --git a/app/views/admin/enterprises/_form_data.html.haml b/app/views/admin/enterprises/_form_data.html.haml index 90f2148c09..0a40538533 100644 --- a/app/views/admin/enterprises/_form_data.html.haml +++ b/app/views/admin/enterprises/_form_data.html.haml @@ -1,3 +1,4 @@ = admin_inject_enterprise += admin_inject_taxons = admin_inject_payment_methods = admin_inject_shipping_methods \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index bda68ec0ae..5120ee4a17 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -1,7 +1,7 @@ .row .alpha.eleven.columns .three.columns.alpha - = f.label "preferred_shopfront_message", t(:shopfront_message) + = f.label "enterprise_preferred_shopfront_message", t(:shopfront_message) .eight.columns.omega %text-angular{'ng-model' => 'Enterprise.preferred_shopfront_message', 'id' => 'enterprise_preferred_shopfront_message', 'name' => 'enterprise[preferred_shopfront_message]', 'class' => 'text-angular', 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", @@ -9,8 +9,17 @@ .row .alpha.eleven.columns .three.columns.alpha - = f.label "preferred_shopfront_closed_message", t(:shopfront_closed_message) + = f.label "enterprise_preferred_shopfront_closed_message", t(:shopfront_closed_message) .eight.columns.omega %text-angular{'ng-model' => 'Enterprise.preferred_shopfront_closed_message', 'id' => 'enterprise_preferred_shopfront_closed_message', 'name' => 'enterprise[preferred_shopfront_closed_message]', 'class' => 'text-angular', 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", 'placeholder' => 'A message which provides a more detailed explanation about why your shop is closed and/or when customers can expect it to open again. This is displayed on your shop only when you have no active order cycles (ie. shop is closed).'} +.row + .alpha.eleven.columns + .three.columns.alpha + = f.label "enterprise_preferred_shopfront_taxon_order", t(:preferred_shopfront_taxon_order) + %br + (top to bottom) + .eight.columns.omega + %textarea.fullwidth{ id: 'enterprise_preferred_shopfront_taxon_order', name: 'enterprise[preferred_shopfront_taxon_order]', rows: 6, 'ng-model' => 'Enterprise.preferred_shopfront_taxon_order', 'ofn-taxon-autocomplete' => '', 'multiple-selection' => 'true', placeholder: 'Category' } + From fcb14f32d2c1ae4fe9c47fe89b1185258f2647a4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 19 Dec 2014 09:33:04 +1100 Subject: [PATCH 430/681] Validating user input for shopfront taxon order --- app/models/enterprise.rb | 7 +++++++ spec/models/enterprise_spec.rb | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 51c12fa011..f0337adbec 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -56,6 +56,7 @@ class Enterprise < ActiveRecord::Base validates :address, presence: true, associated: true validates :email, presence: true validates_presence_of :owner + validate :shopfront_taxons validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? } validates_length_of :description, :maximum => 255 @@ -341,4 +342,10 @@ class Enterprise < ActiveRecord::Base errors.add(:owner, "^#{owner.email} is not permitted to own any more enterprises (limit is #{owner.enterprise_limit}).") end end + + def shopfront_taxons + unless preferred_shopfront_taxon_order =~ /\A((\d+,)*\d+)?\z/ + errors.add(:shopfront_taxon_order, "must contain a list of taxons.") + end + end end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 50b8204a9c..b65ab8b8e4 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -175,6 +175,41 @@ describe Enterprise do e = create(:enterprise, owner: nil) }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: Owner can't be blank" end + + describe "preferred_shopfront_taxon_order" do + it "empty strings are valid" do + enterprise = build(:enterprise, preferred_shopfront_taxon_order: "") + expect(enterprise).to be_valid + end + + it "a single integer is valid" do + enterprise = build(:enterprise, preferred_shopfront_taxon_order: "11") + expect(enterprise).to be_valid + end + + it "comma delimited integers are valid" do + enterprise = build(:enterprise, preferred_shopfront_taxon_order: "1,2,3") + expect(enterprise).to be_valid + enterprise = build(:enterprise, preferred_shopfront_taxon_order: "1,22,333") + expect(enterprise).to be_valid + end + + it "commas at the beginning and end are disallowed" do + enterprise = build(:enterprise, preferred_shopfront_taxon_order: ",1,2,3") + expect(enterprise).to be_invalid + enterprise = build(:enterprise, preferred_shopfront_taxon_order: "1,2,3,") + expect(enterprise).to be_invalid + end + + it "any other characters are invalid" do + enterprise = build(:enterprise, preferred_shopfront_taxon_order: "a1,2,3") + expect(enterprise).to be_invalid + enterprise = build(:enterprise, preferred_shopfront_taxon_order: ".1,2,3") + expect(enterprise).to be_invalid + enterprise = build(:enterprise, preferred_shopfront_taxon_order: " 1,2,3") + expect(enterprise).to be_invalid + end + end end describe "delegations" do From 0fa289a443800bb9d281cf18679297fbfb525ee3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 19 Dec 2014 11:00:17 +1100 Subject: [PATCH 431/681] Changing shopfront message into an alert box --- app/views/shop/_messages.html.haml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/shop/_messages.html.haml b/app/views/shop/_messages.html.haml index 1cca55bb35..1e56aba468 100644 --- a/app/views/shop/_messages.html.haml +++ b/app/views/shop/_messages.html.haml @@ -1,3 +1,4 @@ + - if @order_cycles and @order_cycles.empty? - if current_distributor.preferred_shopfront_closed_message.present? .row @@ -7,5 +8,9 @@ - elsif current_distributor.preferred_shopfront_message.present? .row .small-12.columns - .shopfront_message +   + .row + .small-12.columns + .alert-box{ "ofn-inline-alert" => true, ng: { show: "visible" } } = current_distributor.preferred_shopfront_message.html_safe + %a.close{ ng: { click: "close()" } } × From 21e4f40616f57c7a50ef11d0fc243209ce059826 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 19 Dec 2014 11:01:13 +1100 Subject: [PATCH 432/681] Implementing primary taxon ordering on the shopfront --- .../directives/taxon_autocomplete.js.coffee | 9 ++++- .../admin/taxons/services/taxons.js.coffee | 12 ++++--- .../controllers/products_controller.js.coffee | 4 +-- .../admin/dashboard-single-ent.css.sass | 4 +-- app/controllers/shop_controller.rb | 31 +++++++++++----- app/models/enterprise.rb | 8 ++--- app/models/order_cycle.rb | 3 +- app/serializers/api/product_serializer.rb | 2 +- .../form/_shop_preferences.html.haml | 3 +- app/views/shop/products/_form.html.haml | 2 +- spec/controllers/shop_controller_spec.rb | 35 ++++++++++++++----- 11 files changed, 77 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee b/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee index aac40abd46..b978a050ad 100644 --- a/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee @@ -20,4 +20,11 @@ angular.module("admin.taxons").directive "ofnTaxonAutocomplete", (Taxons) -> formatResult: (taxon) -> taxon.name formatSelection: (taxon) -> - taxon.name \ No newline at end of file + taxon.name + + #Allows drag and drop + if multiple + element.select2("container").find("ul.select2-choices").sortable + containment: 'parent' + start: -> element.select2("onSortStart") + update: -> element.select2("onSortEnd") diff --git a/app/assets/javascripts/admin/taxons/services/taxons.js.coffee b/app/assets/javascripts/admin/taxons/services/taxons.js.coffee index 9257b637e1..4f7faa7c9c 100644 --- a/app/assets/javascripts/admin/taxons/services/taxons.js.coffee +++ b/app/assets/javascripts/admin/taxons/services/taxons.js.coffee @@ -1,15 +1,19 @@ angular.module("admin.taxons").factory "Taxons", (taxons, $filter) -> new class Taxons + taxons: taxons + taxonsByID: {} + constructor: -> - @taxons = taxons + for taxon in @taxons + @taxonsByID[taxon.id] = taxon # For finding a single Taxon findByID: (id) -> - $filter('filter')(@taxons, {id: id}, true)[0] + @taxonsByID[id] # For finding multiple Taxons represented by comma delimited string findByIDs: (ids) -> - taxon for taxon in @taxons when taxon.id.toString() in ids.split(",") + @taxonsByID[taxon_id] for taxon_id in ids.split(",") findByTerm: (term) -> - $filter('filter')(@taxons, term) \ No newline at end of file + $filter('filter')(@taxons, term) diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index 8ed56f252d..5ee0a70270 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -6,13 +6,11 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, $scope.filterText = FilterSelectorsService.filterText $scope.FilterSelectorsService = FilterSelectorsService $scope.limit = 3 - $scope.ordering = - order: "primary_taxon.name" $scope.order_cycle = OrderCycle.order_cycle $scope.incrementLimit = -> if $scope.limit < Products.products.length - $scope.limit = $scope.limit + 1 + $scope.limit = $scope.limit + 1 $scope.searchKeypress = (e)-> code = e.keyCode || e.which diff --git a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass index 07e8ac69c7..dc0c42fa49 100644 --- a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass +++ b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass @@ -1,7 +1,7 @@ @import ../darkswarm/mixins .alert-box - display: block + display: block background-color: #eff5dc border: 1px solid #9fc820 color: #666 @@ -20,7 +20,7 @@ height: auto !important .list .button.bottom - width: 100% + width: 100% .button.big width: 100% diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 2c1b4cd6d5..c1bc34f933 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -6,17 +6,14 @@ class ShopController < BaseController def show end - + def products # Can we make this query less slow? # - if @products = current_order_cycle.andand - .valid_products_distributed_by(current_distributor).andand - .select { |p| !p.deleted? && p.has_stock_for_distribution?(current_order_cycle, current_distributor) }.andand - .sort_by {|p| p.name } + if @products = products_for_shop render status: 200, - json: ActiveModel::ArraySerializer.new(@products, each_serializer: Api::ProductSerializer, - current_order_cycle: current_order_cycle, current_distributor: current_distributor).to_json + json: ActiveModel::ArraySerializer.new(@products, each_serializer: Api::ProductSerializer, + current_order_cycle: current_order_cycle, current_distributor: current_distributor).to_json else render json: "", status: 404 end @@ -39,10 +36,28 @@ class ShopController < BaseController def set_order_cycles @order_cycles = OrderCycle.with_distributor(@distributor).active - + # And default to the only order cycle if there's only the one if @order_cycles.count == 1 current_order(true).set_order_cycle! @order_cycles.first end end + + def products_for_shop + current_order_cycle.andand + .valid_products_distributed_by(current_distributor).andand + .order(taxon_order).andand + .select { |p| !p.deleted? && p.has_stock_for_distribution?(current_order_cycle, current_distributor) } + end + + def taxon_order + if current_distributor.preferred_shopfront_taxon_order.present? + current_distributor + .preferred_shopfront_taxon_order + .split(",").map { |id| "primary_taxon_id=#{id} DESC" } + .join(",") + ", name ASC" + else + "name ASC" + end + end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index f0337adbec..647b7a2990 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -3,9 +3,9 @@ class Enterprise < ActiveRecord::Base SHOP_TRIAL_LENGTH = 30 ENTERPRISE_SEARCH_RADIUS = 100 - preference :shopfront_message, :text, default: nil - preference :shopfront_closed_message, :text, default: nil - preference :shopfront_taxon_order, :string, default: nil + preference :shopfront_message, :text, default: "" + preference :shopfront_closed_message, :text, default: "" + preference :shopfront_taxon_order, :string, default: "" devise :confirmable, reconfirmable: true, confirmation_keys: [ :id, :email ] @@ -345,7 +345,7 @@ class Enterprise < ActiveRecord::Base def shopfront_taxons unless preferred_shopfront_taxon_order =~ /\A((\d+,)*\d+)?\z/ - errors.add(:shopfront_taxon_order, "must contain a list of taxons.") + errors.add(:shopfront_category_ordering, "must contain a list of taxons.") end end end diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index dcf96633c4..134af04c91 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -119,7 +119,8 @@ class OrderCycle < ActiveRecord::Base def valid_products_distributed_by(distributor) variants = variants_distributed_by(distributor) products = variants.map(&:product).uniq - products.reject { |p| product_has_only_obsolete_master_in_distribution?(p, variants) } + product_ids = products.reject{ |p| product_has_only_obsolete_master_in_distribution?(p, variants) }.map(&:id) + Spree::Product.where(id: product_ids) end def products diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index ceeaf7148e..8d4eec36ea 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -6,7 +6,7 @@ class Api::ProductSerializer < ActiveModel::Serializer end private - + def cached_serializer_hash Api::CachedProductSerializer.new(object, @options).serializable_hash end diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index 5120ee4a17..f528bb35f0 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -17,9 +17,8 @@ .row .alpha.eleven.columns .three.columns.alpha - = f.label "enterprise_preferred_shopfront_taxon_order", t(:preferred_shopfront_taxon_order) + = f.label "enterprise_preferred_shopfront_taxon_order", t(:shopfront_category_ordering) %br (top to bottom) .eight.columns.omega %textarea.fullwidth{ id: 'enterprise_preferred_shopfront_taxon_order', name: 'enterprise[preferred_shopfront_taxon_order]', rows: 6, 'ng-model' => 'Enterprise.preferred_shopfront_taxon_order', 'ofn-taxon-autocomplete' => '', 'multiple-selection' => 'true', placeholder: 'Category' } - diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index ef44a133eb..47eaf40c0d 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -17,7 +17,7 @@ %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", - "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | orderBy:ordering.order) track by product.id "} + "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons) track by product.id "} = render partial: "shop/products/summary" %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants"} diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index bfa68da912..d6dda2ff66 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -27,25 +27,25 @@ describe ShopController do spree_get :show controller.current_order_cycle.should == nil end - + it "should allow the user to post to select the current order cycle" do oc1 = create(:simple_order_cycle, distributors: [d]) oc2 = create(:simple_order_cycle, distributors: [d]) - + spree_post :order_cycle, order_cycle_id: oc2.id response.should be_success controller.current_order_cycle.should == oc2 end context "RABL tests" do - render_views + render_views it "should return the order cycle details when the oc is selected" do oc1 = create(:simple_order_cycle, distributors: [d]) oc2 = create(:simple_order_cycle, distributors: [d]) - + spree_post :order_cycle, order_cycle_id: oc2.id response.should be_success - response.body.should have_content oc2.id + response.body.should have_content oc2.id end it "should return the current order cycle when hit with GET" do @@ -60,7 +60,7 @@ describe ShopController do oc1 = create(:simple_order_cycle, distributors: [d]) oc2 = create(:simple_order_cycle, distributors: [d]) oc3 = create(:simple_order_cycle, distributors: [create(:distributor_enterprise)]) - + spree_post :order_cycle, order_cycle_id: oc3.id response.status.should == 404 controller.current_order_cycle.should == nil @@ -74,7 +74,7 @@ describe ShopController do let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator: create(:distributor_enterprise)) } before do - exchange = Exchange.find(order_cycle.exchanges.to_enterprises(d).outgoing.first.id) + exchange = Exchange.find(order_cycle.exchanges.to_enterprises(d).outgoing.first.id) exchange.variants << product.master end end @@ -82,7 +82,7 @@ describe ShopController do describe "returning products" do let(:product) { create(:product) } let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator: create(:distributor_enterprise)) } - let(:exchange) { Exchange.find(order_cycle.exchanges.to_enterprises(d).outgoing.first.id) } + let(:exchange) { Exchange.find(order_cycle.exchanges.to_enterprises(d).outgoing.first.id) } before do exchange.variants << product.master @@ -94,6 +94,23 @@ describe ShopController do response.should be_success end + it "sorts products by the distributor's preferred taxon list" do + t1 = create(:taxon) + t2 = create(:taxon) + d.stub(:preferred_shopfront_taxon_order) {"#{t1.id},#{t2.id}"} + p1 = create(:product, primary_taxon_id: t2.id) + p2 = create(:product, primary_taxon_id: t1.id) + p3 = create(:product, primary_taxon_id: t2.id) + p4 = create(:product, primary_taxon_id: t1.id) + exchange.variants << p1.master + exchange.variants << p2.master + exchange.variants << p3.master + exchange.variants << p4.master + controller.stub(:current_order_cycle).and_return order_cycle + xhr :get, :products + assigns[:products].should == [p2, p4, p1, p3, product] + end + it "alphabetizes products" do p1 = create(:product, name: "abc") p2 = create(:product, name: "def") @@ -112,7 +129,7 @@ describe ShopController do end it "scopes variants for a product to the order cycle and distributor" do - controller.stub(:current_order_cycle).and_return order_cycle + controller.stub(:current_order_cycle).and_return order_cycle controller.stub(:current_distributor).and_return d Spree::Product.any_instance.should_receive(:variants_for).with(order_cycle, d).and_return(m = double()) m.stub(:in_stock).and_return [] From 24cf3dee7451547247c8a51a87321e19ecc0fc4c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 19 Dec 2014 12:57:18 +1100 Subject: [PATCH 433/681] Hiding Shop Preferences in enterprise form --- .../enterprises/controllers/enterprise_controller.js.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index ef175c96db..e17e2e9c21 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -56,6 +56,7 @@ angular.module("admin.enterprises") if newValue == "none" $scope.menu.hide_item_by_name('Shipping Methods') $scope.menu.hide_item_by_name('Payment Methods') + $scope.menu.hide_item_by_name('Shop Preferences') if $scope.Enterprise.is_primary_producer $scope.menu.show_item_by_name('Enterprise Fees') else @@ -63,4 +64,5 @@ angular.module("admin.enterprises") else $scope.menu.show_item_by_name('Shipping Methods') $scope.menu.show_item_by_name('Payment Methods') + $scope.menu.show_item_by_name('Shop Preferences') $scope.menu.show_item_by_name('Enterprise Fees') From a810fc88ac0eb9898ce7f2fe23ff4e7a490522b4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 19 Dec 2014 12:57:56 +1100 Subject: [PATCH 434/681] Dedicated new enterprise form --- app/assets/javascripts/admin/util.js.erb | 7 ++ .../admin/dashboard-single-ent.css.sass | 6 +- .../admin/enterprises/_new_form.html.haml | 105 ++++++++++++++++++ app/views/admin/enterprises/new.html.haml | 8 +- .../single_enterprise_dashboard.html.haml | 5 - spec/features/admin/enterprises_spec.rb | 22 ---- 6 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 app/views/admin/enterprises/_new_form.html.haml diff --git a/app/assets/javascripts/admin/util.js.erb b/app/assets/javascripts/admin/util.js.erb index 93f1c50a64..bc963d4612 100644 --- a/app/assets/javascripts/admin/util.js.erb +++ b/app/assets/javascripts/admin/util.js.erb @@ -28,3 +28,10 @@ show_flash_error = function(message) { } } } + +$(document).ready(function(){ + $('a.close').click(function(event){ + event.preventDefault(); + $(this).parent().slideUp(250); + }); +}); diff --git a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass index dc0c42fa49..567999b43d 100644 --- a/app/assets/stylesheets/admin/dashboard-single-ent.css.sass +++ b/app/assets/stylesheets/admin/dashboard-single-ent.css.sass @@ -1,6 +1,7 @@ @import ../darkswarm/mixins .alert-box + position: relative display: block background-color: #eff5dc border: 1px solid #9fc820 @@ -11,7 +12,10 @@ transition: opacity 300ms ease-out padding: 0.77778em 1.33333em 0.77778em 0.77778em a.close - float: right + position: absolute + right: 5px + top: 0px + font-size: 1.5em .dashboard_item.single-ent diff --git a/app/views/admin/enterprises/_new_form.html.haml b/app/views/admin/enterprises/_new_form.html.haml new file mode 100644 index 0000000000..bb43a421fb --- /dev/null +++ b/app/views/admin/enterprises/_new_form.html.haml @@ -0,0 +1,105 @@ +.row + .three.columns.alpha + = f.label :name + .nine.columns.omega + = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles", class: "fullwidth" } + +- if spree_current_user.admin? + .row{ ng: { app: "admin.users" } } + .three.columns.alpha + =f.label :owner_id, 'Owner' + .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} + %a What's this? + .nine.columns.omega + - owner_email = @enterprise.andand.owner.andand.email || "" + = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email +.row + .three.columns.alpha + %label Primary Producer? + .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %a What's this? + .five.columns.omega + = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' +   + = f.label :is_primary_producer, 'I am a Producer' +- if spree_current_user.admin? + .row + .alpha.eleven.columns + .three.columns.alpha + = f.label :sells, 'Sells' + .with-tip{'data-powertip' => "None - enterprise does not sell to customers directly.
    Own - Enterprise sells own products to customers.
    Any - Enterprise can sell own or other enterprises products.
    "} + %a What's this? + .two.columns + = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "None", value: "none" + .two.columns + = f.radio_button :sells, "own", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "Own", value: "own" + .four.columns.omega + = f.radio_button :sells, "any", 'ng-model' => 'Enterprise.sells' +   + = f.label :sells, "Any", value: "any" + + +.row + .alpha.three.columns + = f.label :contact, 'Contact Name' + .omega.nine.columns + = f.text_field :contact, { placeholder: "eg. Gustav Plum"} +.row + .alpha.three.columns + = f.label :email + .omega.nine.columns + = f.text_field :email, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email" } + .alert-box + %i.icon-info-sign + If we don't recognise this email address we'll send you a confirmation email to make sure it belongs to you. You'll need to use the link in the email we send to fully activate your new enterprise. + %a.close{ href: "" } × +.row + .alpha.three.columns + = f.label :phone + .omega.nine.columns + = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} +.row + .alpha.three.columns + = f.label :website + .omega.nine.columns + = f.text_field :website, { placeholder: "eg. www.truffles.com"} + += f.fields_for :address do |af| + .row + .three.columns.alpha + = af.label :address1 + .nine.columns.omega + = af.text_field :address1, { placeholder: "eg. 123 High Street"} + .row + .alpha.three.columns + = af.label :address2 + .nine.columns.omega + = af.text_field :address2 + .row + .three.columns.alpha + = af.label :city, 'Suburb' + \/ + = af.label :zipcode, 'Postcode' + .four.columns + = af.text_field :city, { placeholder: "eg. Northcote"} + .five.columns.omega + = af.text_field :zipcode, { placeholder: "eg. 3070"} + .row + .three.columns.alpha + = af.label :state_id, 'State' + \/ + = af.label :country_id, 'Country' + .four.columns + = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" + .five.columns.omega + = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" +.row + .twelve.columns.alpha +   +.row + .twelve.columns.alpha + = render partial: "spree/admin/shared/new_resource_links" \ No newline at end of file diff --git a/app/views/admin/enterprises/new.html.haml b/app/views/admin/enterprises/new.html.haml index 634044bf30..8f21067941 100644 --- a/app/views/admin/enterprises/new.html.haml +++ b/app/views/admin/enterprises/new.html.haml @@ -6,6 +6,10 @@ - content_for :page_actions do %li= button_link_to "Back to enterprises list", main_app.admin_enterprises_path, icon: 'icon-arrow-left' -= render 'admin/enterprises/form_data' -= render 'admin/enterprises/ng_form', action: 'new' +-# Form + += form_for [main_app, :admin, @enterprise], html: { "nav-check" => '', "nav-callback" => '' } do |f| + .row + .twelve.columns.fullwidth_inputs + = render 'new_form', f: f diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 6f48b73fa2..f8cb1fb5d9 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -40,11 +40,6 @@ %strong= "Manage #{@enterprise.name}." %a.close{ href: "#" } × - :javascript - $('a.close').click(function(){ - $(this).parent().slideUp(250); - }); - .row .alpha.seven.columns.dashboard_item.single-ent#map .header diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 4a751c4aab..e7f7ea14cf 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -131,40 +131,18 @@ feature %q{ select2_search admin.email, from: 'Owner' select2_search admin.email, from: 'Owner' choose 'Any' - select2_search eg1.name, from: 'Groups' - click_link "Payment Methods" - check "enterprise_payment_method_ids_#{payment_method.id}" - - click_link "Shipping Methods" - check "enterprise_shipping_method_ids_#{shipping_method.id}" - - click_link "Contact" fill_in 'enterprise_contact', :with => 'Kirsten or Ren' fill_in 'enterprise_phone', :with => '0413 897 321' fill_in 'enterprise_email', :with => 'info@eaterprises.com.au' fill_in 'enterprise_website', :with => 'http://eaterprises.com.au' - click_link "Social" - fill_in 'enterprise_twitter', :with => '@eaterprises' - fill_in 'enterprise_facebook', :with => 'facebook.com/eaterprises' - fill_in 'enterprise_instagram', :with => 'eaterprises' - - click_link "Business Details" - fill_in 'enterprise_abn', :with => '09812309823' - fill_in 'enterprise_acn', :with => '' - - click_link "Address" fill_in 'enterprise_address_attributes_address1', :with => '35 Ballantyne St' fill_in 'enterprise_address_attributes_city', :with => 'Thornbury' fill_in 'enterprise_address_attributes_zipcode', :with => '3072' select2_search 'Australia', :from => 'Country' select2_search 'Victoria', :from => 'State' - click_link "About" - long_description = find :css, "text-angular div.ta-scroll-window div.ta-bind" - long_description.set 'Connecting farmers and eaters' - click_button 'Create' flash_message.should == 'Enterprise "Eaterprises" has been successfully created!' end From 813ef463a23ff9c5757f03212ccfae4e72dbe624 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 14:53:25 +1100 Subject: [PATCH 435/681] Tweak markup shopfront page to add icon --- app/views/shop/products/_summary.html.haml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml index 82d5f7c297..2a8430ec14 100644 --- a/app/views/shop/products/_summary.html.haml +++ b/app/views/shop/products/_summary.html.haml @@ -7,12 +7,14 @@ %h3 %a{"ng-click" => "triggerProductModal()"} {{ product.name }} + %i.ofn-i_057-expand - %em from - %span - %enterprise-modal - %i.ofn-i_036-producers - {{ enterprise.name }} + %small + %em from + %span + %enterprise-modal + %i.ofn-i_036-producers + {{ enterprise.name }} .small-3.medium-2.large-1.columns.text-center .taxon-flag From d50f8dcd01c2d4c77a7dd8c951c751ed7b20aadc Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 14:53:46 +1100 Subject: [PATCH 436/681] Tweak styling shopfront page to allow for new icons and hover state --- .../stylesheets/darkswarm/_shop-product-rows.css.sass | 7 +++++-- app/assets/stylesheets/darkswarm/shop.css.sass | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index 1711779cf4..d5d29fc23b 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -100,14 +100,17 @@ padding-left: 4.5rem @media all and (max-width:480px) padding-left: 0.9375rem + small + font-size: 80% h3 font-size: 1.5rem margin: 0 h3 a color: #222 &:hover, &:focus, &:active - color: black - + color: $clr-brick + i + font-size: 0.6em diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 554d3f2f4e..29a5206555 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -62,9 +62,12 @@ @media all and (max-width: 640px) padding-right: 0.25rem - i.ofn-i_056-bulk, i.ofn-i_036-producers + i.ofn-i_056-bulk font-size: 1rem padding-right: 0rem i.ofn-i_036-producers - padding-left: 0.5rem + padding-left: 0.2rem + padding-right: 0rem + font-size: 0.8rem + From 170b14d0f5d556788a4904c3d8f2bb9d38aba7cc Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 19 Dec 2014 15:39:13 +1100 Subject: [PATCH 437/681] Tightening up expectations in shop controller spec --- spec/controllers/shop_controller_spec.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index d6dda2ff66..783ee7fb8f 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -98,27 +98,30 @@ describe ShopController do t1 = create(:taxon) t2 = create(:taxon) d.stub(:preferred_shopfront_taxon_order) {"#{t1.id},#{t2.id}"} - p1 = create(:product, primary_taxon_id: t2.id) - p2 = create(:product, primary_taxon_id: t1.id) - p3 = create(:product, primary_taxon_id: t2.id) - p4 = create(:product, primary_taxon_id: t1.id) + p1 = create(:product, primary_taxon_id: t2.id, name: 'abc') + p2 = create(:product, primary_taxon_id: t1.id, name: 'def') + p3 = create(:product, primary_taxon_id: t2.id, name: 'abcd') + p4 = create(:product, primary_taxon_id: t1.id, name: 'defg') exchange.variants << p1.master exchange.variants << p2.master exchange.variants << p3.master exchange.variants << p4.master controller.stub(:current_order_cycle).and_return order_cycle + order_cycle.stub(:valid_products_distributed_by) { Spree::Product.where( id: [p1, p2, p3, p4] ) } xhr :get, :products - assigns[:products].should == [p2, p4, p1, p3, product] + assigns[:products].should == [p2, p4, p1, p3] end - it "alphabetizes products" do + it "alphabetizes products by name when taxon list is not set" do + d.stub(:preferred_shopfront_taxon_order) {""} p1 = create(:product, name: "abc") p2 = create(:product, name: "def") exchange.variants << p1.master exchange.variants << p2.master controller.stub(:current_order_cycle).and_return order_cycle + order_cycle.stub(:valid_products_distributed_by) { Spree::Product.where( id: [p1, p2] ) } xhr :get, :products - assigns[:products].should == [p1, p2, product].sort_by{|p| p.name } + assigns[:products].should == [p1, p2] end it "does not return products if no order_cycle is selected" do From 1e1a070b2b1bae001a13d31180b6b753de945cf2 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 19 Dec 2014 12:14:57 +1100 Subject: [PATCH 438/681] Scope Variant#price_in --- lib/open_food_network/scope_variant_to_hub.rb | 4 ++++ .../open_food_network/scope_variant_to_hub_spec.rb | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/open_food_network/scope_variant_to_hub.rb b/lib/open_food_network/scope_variant_to_hub.rb index 89a527ea19..bb32a3cbf8 100644 --- a/lib/open_food_network/scope_variant_to_hub.rb +++ b/lib/open_food_network/scope_variant_to_hub.rb @@ -4,6 +4,10 @@ module OpenFoodNetwork VariantOverride.price_for(@hub, self) || super end + def price_in(currency) + Spree::Price.new(amount: price, currency: currency) + end + def count_on_hand VariantOverride.count_on_hand_for(@hub, self) || super end diff --git a/spec/lib/open_food_network/scope_variant_to_hub_spec.rb b/spec/lib/open_food_network/scope_variant_to_hub_spec.rb index b657afeb9b..429bfbe082 100644 --- a/spec/lib/open_food_network/scope_variant_to_hub_spec.rb +++ b/spec/lib/open_food_network/scope_variant_to_hub_spec.rb @@ -19,6 +19,19 @@ module OpenFoodNetwork end end + describe "overriding price_in" do + it "returns the overridden price when one is present" do + vo + v.scope_to_hub hub + v.price_in('AUD').amount.should == 22.22 + end + + it "returns the variant's price otherwise" do + v.scope_to_hub hub + v.price_in('AUD').amount.should == 11.11 + end + end + describe "overriding stock levels" do it "returns the overridden stock level when one is present" do vo From 94684e9963beae97195574d8806080f27cf8f022 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 19 Dec 2014 12:15:58 +1100 Subject: [PATCH 439/681] Show overridden price in shopping cart --- app/models/spree/order_populator_decorator.rb | 3 +++ app/views/spree/orders/_line_item.html.haml | 2 +- .../shopping/variant_overrides_spec.rb | 22 +++++++++++++++---- spec/support/request/shop_workflow.rb | 3 ++- spec/support/request/web_helper.rb | 4 ++-- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/models/spree/order_populator_decorator.rb b/app/models/spree/order_populator_decorator.rb index e281755fdc..3759866236 100644 --- a/app/models/spree/order_populator_decorator.rb +++ b/app/models/spree/order_populator_decorator.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_variant_to_hub' + Spree::OrderPopulator.class_eval do def populate(from_hash, overwrite = false) @distributor, @order_cycle = distributor_and_order_cycle @@ -31,6 +33,7 @@ Spree::OrderPopulator.class_eval do def attempt_cart_add(variant_id, quantity, max_quantity = nil) quantity = quantity.to_i variant = Spree::Variant.find(variant_id) + variant.scope_to_hub @distributor if quantity > 0 if check_stock_levels(variant, quantity) && check_order_cycle_provided_for(variant) && diff --git a/app/views/spree/orders/_line_item.html.haml b/app/views/spree/orders/_line_item.html.haml index 027e16978a..7c11e40bb3 100644 --- a/app/views/spree/orders/_line_item.html.haml +++ b/app/views/spree/orders/_line_item.html.haml @@ -1,4 +1,4 @@ -%tr.line-item +%tr.line-item{class: "variant-#{variant.id}"} %td.cart-item-image{"data-hook" => "cart_item_image"} - if variant.images.length == 0 = link_to small_image(variant.product), variant.product diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index 727bc5700e..ae7bacc7a7 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -9,7 +9,7 @@ feature "shopping with variant overrides defined", js: true do use_short_wait describe "viewing products" do - let(:hub) { create(:distributor_enterprise) } + let(:hub) { create(:distributor_enterprise, with_payment_and_shipping: true) } let(:producer) { create(:supplier_enterprise) } let(:oc) { create(:simple_order_cycle, suppliers: [producer], coordinator: hub, distributors: [hub]) } let(:outgoing_exchange) { oc.exchanges.outgoing.first } @@ -54,15 +54,29 @@ feature "shopping with variant overrides defined", js: true do page.should have_selector 'li.total div', text: '= $61.11' end + # The two specs below reveal an unrelated issue with fee calculation. See: + # https://github.com/openfoodfoundation/openfoodnetwork/issues/312 + it "shows the overridden price with fees in the quick cart" do fill_in "variants[#{v1.id}]", with: "2" show_cart page.should have_selector "#cart-variant-#{v1.id} .quantity", text: '2' - page.should have_selector "#cart-variant-#{v1.id} .price", text: '61.11' - page.should have_selector "#cart-variant-#{v1.id} .total-price", text: '122.22' + page.should have_selector "#cart-variant-#{v1.id} .price", text: '$61.11' + page.should have_selector "#cart-variant-#{v1.id} .total-price", text: '$122.22' + end + + it "shows the correct prices in the shopping cart" do + fill_in "variants[#{v1.id}]", with: "2" + add_to_cart + + page.should have_selector "tr.line-item.variant-#{v1.id} .cart-item-price", text: '$61.11' + page.should have_field "order[line_items_attributes][0][quantity]", with: '2' + page.should have_selector "tr.line-item.variant-#{v1.id} .cart-item-total", text: '$122.21' + + page.should have_selector "#edit-cart .item-total", text: '$122.21' + page.should have_selector "#edit-cart .grand-total", text: '$122.21' end - it "shows the correct prices in the shopping cart" it "shows the correct prices in the checkout" it "creates the order with the correct prices" it "subtracts stock from the override" diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index 6c47d6f706..fb9aa4cadb 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -1,6 +1,7 @@ module ShopWorkflow def add_to_cart - first("input.add_to_cart").click + wait_until(10) { first("input.add_to_cart:not([disabled='disabled'])") } + first("input.add_to_cart:not([disabled='disabled'])").click end def have_price(price) diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index bf76ff9919..fdb63b3ddc 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -125,9 +125,9 @@ module WebHelper # http://www.elabs.se/blog/53-why-wait_until-was-removed-from-capybara # Do not use this without good reason. Capybara's built-in waiting is very effective. - def wait_until + def wait_until(secs=nil) require "timeout" - Timeout.timeout(Capybara.default_wait_time) do + Timeout.timeout(secs || Capybara.default_wait_time) do sleep(0.1) until value = yield value end From c3995ee4d589d2784e9416a962e24317b781c25a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 19 Dec 2014 14:43:32 +1100 Subject: [PATCH 440/681] Checkout shows overridden prices --- .../consumer/shopping/variant_overrides_spec.rb | 12 +++++++++++- spec/support/request/shop_workflow.rb | 2 +- spec/support/request/web_helper.rb | 4 ++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index ae7bacc7a7..c7eae02e0e 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -77,7 +77,17 @@ feature "shopping with variant overrides defined", js: true do page.should have_selector "#edit-cart .grand-total", text: '$122.21' end - it "shows the correct prices in the checkout" + it "shows the correct prices in the checkout" do + fill_in "variants[#{v1.id}]", with: "2" + show_cart + wait_until_enabled 'li.cart a.button' + click_link 'Quick checkout' + + page.should have_selector 'form.edit_order .cart-total', text: '$122.21' + page.should have_selector 'form.edit_order .shipping', text: '$0.00' + page.should have_selector 'form.edit_order .total', text: '$122.21' + end + it "creates the order with the correct prices" it "subtracts stock from the override" end diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index fb9aa4cadb..505fccb03c 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -1,6 +1,6 @@ module ShopWorkflow def add_to_cart - wait_until(10) { first("input.add_to_cart:not([disabled='disabled'])") } + wait_until_button_enabled 'input.add_to_cart' first("input.add_to_cart:not([disabled='disabled'])").click end diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index fdb63b3ddc..a77f12d7dd 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -133,6 +133,10 @@ module WebHelper end end + def wait_until_enabled(selector) + wait_until(10) { first("#{selector}:not([disabled='disabled'])") } + end + def select2_select(value, options) id = options[:from] options[:from] = "#s2id_#{id}" From 2bad590ef4eb0adbbc578f35b92ca4437625ae80 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 19 Dec 2014 16:19:56 +1100 Subject: [PATCH 441/681] Order confirmation and order objects use overridden prices --- .../shopping/variant_overrides_spec.rb | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index c7eae02e0e..8aec1079ea 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -4,6 +4,7 @@ feature "shopping with variant overrides defined", js: true do include AuthenticationWorkflow include WebHelper include ShopWorkflow + include CheckoutWorkflow include UIComponentHelper use_short_wait @@ -13,6 +14,8 @@ feature "shopping with variant overrides defined", js: true do let(:producer) { create(:supplier_enterprise) } let(:oc) { create(:simple_order_cycle, suppliers: [producer], coordinator: hub, distributors: [hub]) } let(:outgoing_exchange) { oc.exchanges.outgoing.first } + let(:sm) { hub.shipping_methods.first } + let(:pm) { hub.payment_methods.first } let(:p1) { create(:simple_product, supplier: producer) } let(:p2) { create(:simple_product, supplier: producer) } let(:v1) { create(:variant, product: p1, price: 11.11, unit_value: 1) } @@ -24,6 +27,7 @@ feature "shopping with variant overrides defined", js: true do let(:ef) { create(:enterprise_fee, enterprise: hub, fee_type: 'packing', calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } before do + ActionMailer::Base.deliveries.clear outgoing_exchange.variants << v1 outgoing_exchange.variants << v2 outgoing_exchange.variants << v3 @@ -88,7 +92,54 @@ feature "shopping with variant overrides defined", js: true do page.should have_selector 'form.edit_order .total', text: '$122.21' end - it "creates the order with the correct prices" + it "creates the order with the correct prices" do + fill_in "variants[#{v1.id}]", with: "2" + show_cart + wait_until_enabled 'li.cart a.button' + click_link 'Quick checkout' + + checkout_as_guest + + within "#details" do + fill_in "First Name", with: "Some" + fill_in "Last Name", with: "One" + fill_in "Email", with: "test@example.com" + fill_in "Phone", with: "0456789012" + end + + toggle_billing + within "#billing" do + fill_in "Address", with: "123 Street" + select "Australia", from: "Country" + select "Victoria", from: "State" + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" + end + + toggle_shipping + within "#shipping" do + choose sm.name + end + + toggle_payment + within "#payment" do + choose pm.name + end + + ActionMailer::Base.deliveries.length.should == 0 + place_order + page.should have_content "Your order has been processed successfully" + ActionMailer::Base.deliveries.length.should == 2 + email = ActionMailer::Base.deliveries.last + + o = Spree::Order.complete.last + + o.line_items.first.price.should == 55.55 + o.total.should == 122.21 + + email.body.should include "$122.21" + end + it "subtracts stock from the override" end end From fafdb29fcb63225a67b87c81932ea6feff004dc8 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 17:04:26 +1100 Subject: [PATCH 442/681] Regenerated icon font, smaller file size and updated graph icon --- public/OFN.eot | Bin 41972 -> 51656 bytes public/OFN.svg | 148 ++++++++++++++++++++++++------------------------ public/OFN.ttf | Bin 41824 -> 51508 bytes public/OFN.woff | Bin 26600 -> 51584 bytes 4 files changed, 74 insertions(+), 74 deletions(-) diff --git a/public/OFN.eot b/public/OFN.eot index 0329e7ac1e04dc7335e89aaf5a0477a63ed085bf..b1eddc19e907e35d53519dddc69fbede3270a9d9 100755 GIT binary patch literal 51656 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 literal 41972 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 diff --git a/public/OFN.svg b/public/OFN.svg index a50fcc7687..94db6c7ac5 100755 --- a/public/OFN.svg +++ b/public/OFN.svg @@ -3,78 +3,78 @@ Generated by IcoMoon - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/OFN.ttf b/public/OFN.ttf index 51423d386053203737e1dd8ea131f5a8d33244d2..f901ac82d6dd541089bf3c3ab242f57d65557b1b 100755 GIT binary patch literal 51508 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 literal 41824 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 diff --git a/public/OFN.woff b/public/OFN.woff index e58fda0e071f16a6a47ab04a92f7ad342fc91315..5c54beb4529b7d3785e56c1610ccca848885d5db 100755 GIT binary patch literal 51584 zcmdtL3z!_&bsku^s=K7MDCo;NT+1C5yh0Sqx9fB^vl z_m9#4l9}Fn|AQxR-M2r3Yo4P2|1|px{KXKawAIGsBxNm^| zfAQ&B?&O1~K7sx&wy(2LvI2%WZ3GR&{0;M8Ls296;NhCwPqk@tufg5{{H;F zkC_)n%xr6zZ>cYSW#`WQ=Ao;u`o;+^s=s*8o7XomfikR?W$SVG2K$kR7q!HbGlKOOK`u9uut|$C`ERRabrU z#Nu;1=MMOa=9@&47{ulmAC|!~Ejthh2VS>q7f%iZ0uft%3kN{Rj95MzeeAO1C&E20|dVE~DqyNeh7qs23WRWW-XhGvq>r^lrvSY#K+2*%U3PpqTqZSm1E{vs% zzS*W)BxD4cDD?O-x}dqN=IC!A#y9^q4GTaG=haafk5Iq_E!v?!PssX z)G*BVIwkByF;2B%$U`D$HHW}$)(g#(MB)wx9Y}7QO|*;2R|Nx@JSNfrPO8tSQ~vMh zkZr4^ddkESqMif;&3jE7BptBAxB*KHL&?Y0r<$Mjz6IjpP{O7@C>JdIUF+wJE%?HW zY_wyDoi%$x!#%xa)O=Sz*G`1P37dY@!_CLcAPpRrGPnd9!=IsmdMb!fSL&?vQ=Q@K z6V#aIr<%v zb!)!ed{jN6pNj@J@1?QEaxgKPPhs!^;UEtlz=`J`tHhjFpHPpYgJ`E6w1dq>&<|RK zuHowDNpE2Nvm-R}xUt)M$eJ|{T+-f7T3$WS1PZH(g?b@UiuC|fY9J5{#%vIG^IJ^j zAow7Tsk!Ddu_F2b!SWSqR?Q04qv4=nKF_paF5%p~T*YhPEWXiCdjV;ZV%nq=>Wb#K zeJe+gg%z-A*&3!grjq7a^Q2KUHi4@A%vnSs5twqDiC^ckB#+ewNrsx*LV{Ssbcg1O z#d5h=oEzd&FG|Dt@kBD6PA10lh4DlpokFQ#p7bvAN^|K1&K1UziL_UObZ%rWe#`m+ zWWl;|oAI#mRpZ~Q7%1pLbw>RO)=9&>!~8Y#74vV=W^S5>b8et!klajRjnY`Omzo^> zXg74}{70&+zz?W83LB& z+Mz@!oN^r;P4H331_1|0!{LPr$T-!IgcuG!(UiU{+K|C`iH|sz(JeI}pY(91jO<0;90iASN8dF<}lk zhnfJyz%#0{1$pv)WxpK>2Hl{qZpWtTBX-cWs5PV(fn7R;b!Z;}90Wr;44IR}7K@kx zGp``=Esviy&j+1g0DK9x(DgR13kL8k90mVEW8^5l;83g>iDbe~EI{ZSh&kbGBvOp> zQ70S=0>uNtSlG$nXsr4DXwG%hPB@CD;3pb((yp6}M)Notc5oC^5QsWq9L-13YVV2M z8+js*2GMY89Uqm}t?Y|r9VZpGU22CFh=f8Z$H_%pJ{pRUT--kDS`8Z$gCX?h1|5WC z6b)ivy}7{6zjYKP-QB|=eQJdxV%hC_7JHD^Oqk&Q-T5{XKJQ80o~C~oVc z5Y$iyzS!|l^Sq`h&?Ty|oG>U2GzArb2*trM(1aU83J5dvI8I;8YoOU1~_QG&2)>FqD#8{A!-llh8SWy?&!YKIF5skum)I)Turvr zH;^NfI1#gX(U3|o>>yhVyAzPfAhHemh?y5#fyE`Nfg~uFy4pe%v>YMs zs1TI0nv{J8}kh9T9AsR2bk&F=YWA96~J_?`3n2-R$-Ejj! zC}bd73?3cjB^BVKXooW2W(o?Y9VhF!8OKdIp$HCPE|`*Cv=40n{6x{RaXogRETS}< zfB@B^a)1ocTNprKM=+we@S=gSC%6lTQnb!ifgm4EIXKERQ$Pd5nqonKz%M`n(;ywi zj1w&4C?n%Zz8L5g@}Ln<#IHwoOvVJGke`BvdGT$lX+33(8rK>38jl)Z!oh)-=s^;P zs5%W1I!M&bvr!9jlu!R?nYbDQq92LEE_zBWB?4DUBrcJ}QX-0l{IZywkrPvENpjYS zm2HLbbh_+1jxu9Wb!W7aOb({ggUMutzQs)CIBst`HBoFn9V^9vWzDCYs4{IQl#j;; z)8@a*48~*mP&j0&sN4J^kPVkHj52(AB`cbD!mbkz7hxBG~Z-wT}DIlu{aunc7*fj0!l~QEg{l$iN*o}n%9;cjBGT2 zV!W-O(sbG*Kd7C?@(Z6Lg0BADGCj3JjfzGmnh!qlb!4#Smz`=%0IP3?MC%i1MUcBx zoD-Q?J`<#Ul~qO63Llz$x*Iv(*M?Lfehlpl0)apXt7NuG6BB?Lj`=m2@UN{y10G2*A(UsqBuwy)9)R!T2+pEsXi@5x7SCIG=G0$y$tPXNP)?4qARLyLPVB1Tlc)Q#+qw8@^7&^Fl{ zWM^0;h~-@w#>A87=>w4R^(NOw(rq`RzT@DY;D0@2Wg zC9iBPU08}0G0B+aV$52K6##Br_OAK+xqg}3;yr~+RerSBATBB6GMBd7%Pl|ufv+@- zfH8mZy!E@*8R*b|Y+O`Ne*|6GS_4|zX3$k=B$MX{p)O^%{fj!SRrNN*B*VfEwk@oI zb^hlyGJbUATIFf})@d7~sG|NG{D>$f4Y3vYKmG9QzrLf)47bC4*kB zVeybP3<5d8U{H}xl?pr1JRq(KuWG#lMh;xtv6kbC7Hy8;jr$rLs2}#dp5Zeczlk51?&g z{bdU_@hl7$wfHheF?FH`hRMP}GXo9Mfij$gVh0<912Hkt6N#jPWWf!lB9Wd%d@$os zA3{#j!96sgxEVLZS*nH=0j!M35_CrP$*57ixMF?ZT4FokuNdDpE~sBs|Eqe>95p{~ zK4b0r2$(kD!?X?*YDw(`LO%HU2nIvmeDl}6PDJ@pwvNqk1N~qf)e0^ynZAZ=WM0|F;vVDsb0Ajh8m3IwfMn%4%@uRW7-1yM;GLa(W zp&eM-(uW92eG>AIW>wUklL^OUm9=R*#1mKW51?#C)cjJ84Ayp0>}D9j3FR>~JWu{< z5@yoTSU>N$^iq9;kFoi$_m^qwlG^P#M|jGk@Eu}0z9I87Hh;NhRY{2k@_05StWlV( zx*lCw?tZ-cQB_U##p6BEXqvZi>~u8R6OZ?$x*t`~1|S>*&B^mtO(VQWZ-1#!vn6o{3gvAjD?rm9GgG!IJw-<-fZq!*CA zA^_|qvscD-GAeJd{~|G}f0&j%3v{XeOClcLSRrOv=Qu^pvm>j7+h;arI)N93^=u4y{L~g!T|g9i^Fs8JYJ6{c?=8 z=<7*$%KnH)m#_{lzX*IwW64aWl2c=#CD;Mkx)({)GIb&4^~9+w%%Vve!w^7@Gx3+c z5%2xY(o^M|*ilb*WhrSBoP6n076PvC~^+dR&dIn*j zV=F&#Z4$=#mxox4Zfu|Bq9d)-f9Z*ve@d?@RmLY5aYUE-~Q? ziLWO7OP0eD#toJRCXAL~4DetCNDU;D{RyFXj5lQ8Xc$AgZdX2+&*yWy*SnC;;<7

    j4|Z{i)1sVWJRt&a+$h)q&cJVQ5&N&}j2nzk8_yUo7~g8iS`}?^ z%4s8{HVBA)a(ioM*kWcV{7D$9=HJ>nz$~t!L?x?;zs-{9k12K_zYiT8eVyB*zU2{oa z8mncMl$W^!7nb=+;S>7DY<{q%XPE%pFO_=Aonaj)E=qk7K{CRFQ$>%VY`g}H@ zH2xiEL?%s7@0vv5w_MMxJ)1%om_4qqD}AhMQJf|QAt9CiQt zNx9J?AziULo&0-3w~w5!uwPYe&QYflG=%TG8}&nY5o)5bz+2e%FwM8rG(XX_buDa` z(DsTJi&^>U0v~$Pg)01a_<;&Y0WM%aw0G~x*)3b1xbC_qwrrU_xp(hFHkjf5rFwnl z)*Z8_^7&Jp5VYFismU8-Iq?$)k7a z&~=Pk$0+z=_k)d_JcE4GfVc7}C?s^RAE-r_X>Ee}aDuLCYhapZ=$$Zt#zqv|%_kpj zn)*SzrWWQO-Y_{eHM!y8d9QTkgX0tR`o#EyS9&Fn#JrM6nqJ8~3GN8_zxj^0sqVrP zAn&N8cSMZv&37QkfGj*Nt&#x}iXaHJ_;&Lh5h|*1pIx9UMQmHZ>ZV&&byeHB7v|Ec z)^k5r&U>p|N;g-r-j_Xi@Jbh!c*`4Y=IsLM6WPGgFW(XFkvi!UexygZ3O`y-=02(< z_bI8&g|^^{q6nU#JsYtA%hrnZhsG0-SgusJs86e(Hw@Wp62%h}xD#hExip=yvhtSH zgc@zF?Y1#ND6W93^r303?_%2NCMt+&*rtMlL@AYg*TX zK})7h+jL5Il3GBEs>^9qlKB#&}$KRkL2L$!y4&HuoJ5e zCrB$~V%r;ENHB$WC5Ybc&Bq5anXx<+Fc=2ZN!PUP-dt{~Qr_PC;qPyrmiuX=b~sbd zVo!G@nJ&O3!v(!5wM2?bK*0wErp6yzADqvPf}HF)itn@`HktnH83P}%dT^oCMTqd>X`bl)X< zE<}99euV5y6e@pqUG_Njxh_M!*t)5wBsW zfBvQVWA&Hpk|7I`)+@lqhc7tJ2L*d(1bav$5KNjS0Fd>g|7dsuI54)zqAF;l0eT4{ z=~yEjPUHh4gkbPN0jiJy6>NtMu^(2%2U@RCAC>`9TFaKj-XJ9ci12JjvodN?`nZdxF%r!+5RcmI{6x>OLuL;K3eV_#PLOtOY!0*d5uO8!0}Ol5t-rRfGB8l7T({mU4PVvM zQ>pY6cMlKmF6z=;w<+SGqi&}{-3AR)bSWv^BEwV%i!nbaDSXK1a1_K*XwmS+g$~4+ zg@NoscW==+b(N z3V5ULa#YZwHs522$*3DwO(svPdScad@)Sc}G%sGf_%g?R-fH{{Xeg|?3n1wbor>zv z=~+aBXe3C8Pc7jIw6v8!zKrm;y@dqK(mPWXK%9UiVGJW_A`j5%Z_KU`3&bJ z8hNsW+g4#ip|G*I<@OC5ZXDWwK&gFKpS-Ywnu3A3E0x-{tN*~*_)Y7xW0}lIn(TKD zK^bq&+kyPLe13glGX;$f9XLq$-s`pDEXyfC3syZQ^(uoFS3G+>NtC399CW`5q8sR7wR6_AdqC2oiPqKRZJnfx=M|qx`Z5ss9Q2WLZ zNRo5`>$zwa7H4d@WTO|p=9R4*FMKWCAC1Oh(P)3#x{1pwL0_6jnj^eIwfOUjdPRgv z5QS+rN7OkCMe{jYU2Ok_gY8WiKbm-!H!m#epg<4;X7?tt5rZ!fRWzR?+x>eES`%p+ zaBUAc*s;k(Oet4m(MUDjG`xnHQf}4}!bYA7;1-bQPOFprFlk_O=_Lj&c?_OoHUEol zFiB@b&7c!Lt76;?`Ob1?z zS3C@j0Nyy*^Dqyy$jsZF3(1n}M)|w>t1K2rT?gJbXR%e32p{rly7YFcX+dh@BJ9Ay z(8?Fh`&WL|JQ<*mmD8*LTaQh6M!AU(QjWTja^k{M+-`1-b#J2AjYQQ!aJOJ++&eM; zLCV~%r=M=VbNFy`>BbvthYz3P-)8;rVWy2a!?l*JhhSZHjJT08HW>SjJB+*Ghd7WL zNbv`ae{PsQG9n;$^NwzO$Wv^Ds?mH}vn!9853hX1tgoC?f7!fKeX;p@wa|P%2@#B= zVMuL82}2(aJjF60774@|kUZg5DDV^slJoRBp&d}XQ|bCi)bM^-@U?{h1u6SatHXtgxZ>SU&KLjpBDKy>8Kr|W$$`7QeMBqP=A>S_YLdX6zHs%SCb5S*$ zt>VgfY|SgNq3DD|xv2Rv)B`K958XgB2x-Y~gU+GkZBZ=HUUB#}9IZLnlcW$Y(}9-Y zyceuw(xZt7-cE8P1ZdO=B!7Tjr^0uF1G6_UX$^2_JB~rK;ek<(HlIPms_8Vkkl-e! ztI=5eDQ=IyH)!KiANWcm4LUsu*)Pd!eaP5p?7>>U);MhZn6Zd>iU*DV**IhTOIWV| zT-Av;q#CIKd|0h*!w$hv$OqbHTPDP2Pm96#!Qq|nSB+FH)vhU}ax=NcR4p_^heM62 zMyN3}Rm&+nspj9y$~Amd2|*?l-6?Y9)G*zj15MB9Md4>GTE7mhMcFg$=N5Q{>FZ^M zT>9X3crddfy)dNOnr|?4?6LX1n{HZpePYteLtTUU26GIzP!61cI-C?R2j>83v>226 z%-r0{v&W8E+a@Ph&P`03_Xn&md%7a>!4`gNo~XrDxUSi z<7UlMeA5Z@=@tBMSMp}1=hW$*9aQDv!ov^eC(T(pzw#>A#ZT}m!Q#itoGldUKzz9B zAEeuEQP%9N1RX{&Z1j_ixC!`1l5q>|r-&kq2=sU=?dPZD;jaN<+j|HP46egy(D3DX zIC!-AAa|1ebjW4IwrC&77{U5FuKoG{iSeTGpD8YXSj#)IWSGt>Sm~rL)jraxmKjZW zh^R~bY5ZUFK&rrndDma z&T~fs5g4!?j@=8m3Cxe(xM(y;B#1}E1%$lpqJbNqO!Y;gG3>#V60`A0q(5oinCy#0 z;%YWg!qyB#^1jqDP)H)Y@->K9pb@J!``M5kOolH!!w2}}b08P#K~X}w9Ip|HZEt8aR0;s-oxPJEv40sBEmGjLdoEG*z7 znn8zgP#7Y2rH;t5NYZMaN%pysSS;f9C6UepF4~o3okwEj)qvM4wb=Z!dcnc=H<$}z zUh9174C#c#lhEJKeU9X2EOjtEJ4lHs65)FDX!kxr7CMVu0tcXL959Z7r~H}mmhoS} zOIlJ*s1ahZXk+V08{BJCns%o~8&fll8MY|&D}7XUYIdrrhBYN(kRX1)$_Vudh@;v} zi{e?pl&cCkekdLwClkuf>N9wZl(dCGTc)MD(J5{PEmW~Y>%*u`r>(NMVrkt>E+f~I zQj1@6qGw=xxpz}OX-y<*w8K-&&)nSKH&E^CJF>NBre}ChU*9wWW$Kodoh8By8h4YDcDFRprjytfFEhENU*j@c;^_!sPmfcFIDF-NLGLWw! zps)|=HJ$OcK*LXyLL{)DF()8A7ogW(7+e=rG zAP;F?SuVCEg2<<`cY>V#!jxtQ7eXpWv<(h8LCQQ350X*H%&v#HQ7mpOtz>awQ@NvRiDr3m^mNBlqWCcU)B|@3#2VE!cU(=fw8i_!DJ*K5+ z+kG4BH&kKI!agv?hl6RuM(a|q$PF6H7*l}Q7cU(I7%3vFFd5iVz#KvG3({(4r%ZBO z#_rG!_4<8R$b_T0mU<_j%VFwM8_E?L0V}t)QrTL#@R%LOQ=p%cxd77!xj|erXza<8 zHxVAiG1@wT1PGsa91q(RG8&Qg$)@wO$9{AB_Pu+zZ~x6>$9`+O9`rxO7|`rarYsui z{7j`Xllz>E^e@!?W4^N3Shw!fu3ev4zrL|~y_D8%QM(aIjn3i64xGoML3;Oq0{DlJ z8m%=?0IDQ!C*;njjeetM%o(p4ud6NU*UVk!A@d#+G4iVBXvWvor>`~~92i!j$Z6qE z8+gcsfJK}F;0ZW1T1&Vfp%{b)gCiRET7V|}8J`}DZGqdR5H~X_x529j)?C)D+JJ-t z_CzV%!yl$NQ)5N2g<7K%+&30;+fSB4*XK9#o7-^FMm!Xzrnh+99hH-TxwA)ITDN6H zHxL8F8mqrX4@RZ?u^H7twX(;DbALZyBPgD{{>}XsSc$WkNJg#5G7H3Nx2M)IJ{C>LG5R z3bCoc4FMd{NC5=t2alog)W$vP69&xRFYZ6VzBg=gCnIuuN;Hhd4?=9b|@1w zjaV7_-+S-9cj4s_F#tkSq-X={u^r@aN`{&S?26nY9tz^MC5n{d$){n&Lx^Fu60t~( zhK{u0NL`^ek^}IA0p%&rO9gxk2hm|_IY1uNAYmcQXlQPN=G^(}7x^3p26x~;t|HKf zBbQAH2{;UdlD81S>3j?=%HeB~_CO4b#4dG87xvD&S1$&PT$jaw_8^l2Geg}fY9UiC z@xNB;5-PRKsGc0aDL(5_%`p*mMRUtK=WHU{Xr$fXi3Ao7{A zKLqS2{f*)Wo#8rA4K*G8kmb{v+JARo$o-)qQ^#JhuhJwUNOt3^r>6 zKf5xx0+8N^&mQC>^9l1S&0pW2!Qtu_gCp*S`#U+vrO4i9a0KoiSDQfoPq2>e%_82; zN{D<4%R`)jzxJFsfBM-_qc%$Hhz|XzEM#TLk6XZFnra^&w@6+uSfV*vTa+G+d5bJl z0ZCf&1fXO*6zm2S;TPmB%Uu>7(HB;quhygbM4 zCd?wP=|Pl$8O=jL&C}Q_eI>C=bOw;9CIBeQ)icXWOUncWuJJr<1`Du#m5l3+UogII z{DJZBjlWfCHK`VW2wJvgIT7K*ELw}yZFQJfW!?d62r%e|z?Rz~?bhJ5O=6mV!DF-5zopXA1JsOKTW#jP zwOT3rg z&-#wS_BM`b$2Rdfv$~;L-CH0tQhC0=e`4kPlhyt`lr@fjOvOi$yA=^C`3Xd+C9s%T z!cyCWrNg~(0NZ>S5G}C5emGi2K8!TZ;Wt$!v(kz>yH;zP(pYYc*PjnsI*W|qUw@wX z6J!pw>m^wnFHsq0VK6b$ZNVz(N0%-TlePn=w%s;1I-5^#A07Mnj#J=~{L?<3*)cYD zV!Ir#p4f3>bYwaX6C<1*(_2SI7Pp9G<77Cn%VZ1Dv_ z&B?MxuG6VNT^ z{XMIPP8rp998cZ&nDGd9@?SIlE4*mORYM(AN01NqSJXdOf2{so{a?U^9`g$GsCk#U zWc~y5OXfc}zh(Y8P=Ca_#yW|$-ddMf6g8h}>R+0EZaAexNn)O_Z}69S88$Qq_)kFj zmUBKOD<{#QT3zMSEe<=a4b%cA!vY$-(sgTKG-Rys>$+32Po()VpVs|=5W21H0XVDy!G4@_ovj*s2CWy`H&<2$pH2N3Ps z{b z$u1X=Gg0dT%CDP&F9l`Epe!PJ*OFWpGWw=Ed)bpDT@%A*R<)b^LL44`niykhXg0#e z5MriO_~D1Wp?MrE3=~2Ez_}=iX^I?yJo zjr2_^{Gs8tYFA{ld3C-k!NUExgJ9>^tbnUm%kR4~eCtJ>zkHjTuVT=nIVSmjO{tXo zmPuaLM01+`E7qWXETecOo~Xnh^48886{%M{Dk|@vp!Jx5yQ~R0D>a2jqE3wh057(2N%#x{A4gw;jBxfKtDtYvEZ9T=%$%PfE?1Kts;}RTBLS4O6zvc zmh37NvEr=_BnfsG@L)fIf1z`IkhJEfB+ebJhCw&eX~5 z*P`@8m%@X<#@9QRz}!c;=ktD?qCxI06t1j^=Dc|6x#FxoSN|X+->hDqNZ+_T#LFX~ zyOAxY3e|4W_%-7j2tm79{Ujo3KZ`dYyaZ`Z_$FTJ(dKy&a3FfIeSDL4Q4q!rH#HwS z_ilh3IFPE5Ss%N&{US*SFVxHqe|UF=KYeUdPW#y(^g~kjgn4%CwhF`4VhVqeEdcG= z(^92K0FblNR0yjr!w@!ML^4Bfh~2|wpLzSHlyLV=exaa`5~po~Fu)BbeRfmbhGv$_ zZl>#6B5BAH#~-dpIwE70Yl!;mYnPTGR4s=#Zo|99Mlqb&4+%{*3N`xMGJ5%k}x(Ve1Y#Ws-eJ=MzJK6ZRX$GLdwO@i;t#pc@2DinK{PHe3z{v(2~c5|#`? z#;nT5R4P4;gb$lH9ZWf5%)H#qFXLQABd6~l{k z=aL#Mm#>*H-+^(FxT|H`{Z&pJg77uQ0fkodCq-Osx(;Id|i5}NYB7Y-vEpuQi{Jq3ViVaI{i-kCr%y1^2f@-LK z7fGUviFglZX?1$yi6Tl5!4D|SR#A#nMV3f%Y;BIbUTpOC&h+ltldl&G8*^XTGV}(T zNiPyfa)T4;44Pi(^uHBPTy-+mOKHepSSL4M?2fYqC@fu%BFGl_}4y}jcJ@(v^k35q7&r_4SGm2t5NV!tGqj5l9f zHQqc2JjkmC-rRN$&wA|z)NTAu^Qvk+W~qlCT_Ojy5FGJw8wL!A9)6hS9m9$lB6_Gj zuu^)!eCy=NmC{M`t(C{|fg;(|-ne+qny{Ydcji!TjZXui-&94lN#V^wqBdU&Z;drm zvUg-x_5;tFcZ3+nKFl+8XcL25LMwh+?+Iyb)CYW{Q&z*zCv=#UzddqOA;aM7M?Sf) zZTurWiD$7U0m~nz(aHsH*FhHS!JN25XF$nc3Idd(FhABmLDOP#^6@l0@@ds3(~kE0pV* zbPjtxFhjHd3H(=r?B!z)SK%i1Jo1f2PYay(Gv`yUN?@qfIPU0pRQJW{XrmwOLPB#_v# zH=414cF^MRmIq3{SOpt^E|eR=c(Z?NNI4Y|6(w!$x@*e4)hczj4vfStA~iSP%nc&= zA(u&S>LqD5>*7@>$blF^GDQf@{mIl|y8bcTSOLBvP#TH|sh=1Fde;Q?JcD;ybPxu_ zCnJ2ZX_Nv}Z z^nzRa1e=j4DZ#87ooRWlau3*v0aZLl0x=gGGRM5u1g305Ayd`sO2)`xp`ooSv+}ZZd0g zI4tScwt0$n<8Tx2P}2wHorK6lRq@vNh23!2ra+@am>!3`DE;(Mq-83!rJOR~V6LBNrk6b$W>p#sJ;%-igxrYAG>HHIpB#X2Oqr^n=w* zPIU=qDH(;IIr1BlZjWEQjwy%a=aI}>$1zTrK% zRs}nGR@AlM36YS2r?aoXDaal~TGp%daLggeXoxg3!{*AP0JrnYG=J9pm{FX!gN;gj}Kj4DGzNJ8@+#a_P(*P z4MXM1)kEV~6^cFML&GPwZ#ywOG|^KmTs1yHf+>^5==NMTH`w2Qq_O2lfBztSN4F1S zLUEeuMYjGBphC1^sU-WQ(>V|tKnGFa-yR>TElyAW_|VXJGM~G4HjfLba%ea71ZZv0 zfUtb=5D<+IWM(NVaz}mC9sNsmb2#2;OaA&lCBRrf3Dz0NZG?8jB?paHda1fyQab zWVBH9YQS|^3k0ULTmq#M^^x6QAX!04!pTW=sx~n`H1y-s`TbXHefr)J68~{k$cj1a z3uYrwE#P2XV|CRIpWdOvT)qe0+EyLhUdWfsQa-nRu-r&zau7ipW%MtT%V#p1 z@#=OY71B8_i=bCy&9K#sp9V)oB$n(oB>7?&OZIt~o5~(XL(0A_uL9%=aV%}KeIZa@ z2AbxjY#R5To!@)?^?T>f-s_cK8yy+>$^C_O+3awtBri%zVSKaNCr%uO()|?Y00Vc- zcGCz3#GD7sOuYxrLacG$`gL33 z(UwNt2ya8gMsE^m29nIgkoq4_Q4kj~=`l$^i(ryCEF4S>xzTtc$#OZkCH564m2MQ2 zLJSTOK*%x-dCto>S~M!gv~m10^I=i2sO8tE{902>#*>9hA_~6|Db*k=Piycis{1IP zK5?SKDZ3ED+j_SZvS7#6w@yL?%_YcA>5!e}Afh!F z@|m|T6?3(zZ_V_V+KIO|AXM7ne_dgBVeBvZh8IrI0)G<9H>+xb%;qc6Ll3YKh8UPd zftuJ>N4oPQz5t-&6cFuEuvbzBt0~P_JUvcr9%j0VXnNZhcZ;aRv7 zcJ$+)U8WxrA)_~_xf8-^{e}VQiB<#hn~Cj8rzMq`Sdd#Q&yS_T=P#8F$g(R~*ot=Q zusjRP?LKUXoI!Yu2?srq;E5Szs3D$8D2CwyH^OH+ zncPt68H>f^$c#}*yNEjmP!TtRG#Z1#N|=i4=*2jMe)y?v%jODWlpC5;FLSB;%&Zu= z1F-yn-iOT%Ss16EJ$@ zd%?sI84V|8Vyrn)bD+=n#Gz*d?U7<}1B|njusl0e?Z0}&YZ;e_jy-IlJhDHBToV5j zRKS2RIzpo_@>PR#Cyh!_zD5Bb_SF0+yj^bB1n&os?JA@pgQrgBR?~}L$Q!QZYV7;p z$Tf07Zj7I{G|hpHN;~nXekjqVi5^%+-~~jHr*V|SrtQIvS2_%#r_fW#<+cq}ct0KE z959o;lgVTXOMi3eOsx^kKwyIVHeS305`h$j)t1bG5VD9y99H{xY}tX|=3kOA-wozr zKF=iM@zFwcJ+dRvyhR-3d@v8d#2yEajKz?hTR;aPl}JDEP`6dPVjz12gg?}lrI@f- zNHcV@coz_Sm?;G^(J1fy<8>Bz>jil{#_<{`D?(WuQhevTv$GBSQZ6z)m|~4N2p2Jx z{n|f_1D9OPf3(e8YRJm)abwB&MZ6F0e>0lOfi>j;^$4f|lVuOWECRsKwW30_^n)c% z;TgFZF`;md+b~gkLq%ES@AdP{L{sM66oht0tKs#C1e8$R_Yn56>on)D@`KyVNMEpX2yr0XD5?GT80Gh=Q zdX3hT36!>+xNbN!;HdYiNRpPE47U>6evRRK@q~;6gF4z{sn{oPLJm?4VdF~xo^UyWa$;Thho270Gw)u5>Am6gM6czQ1Mh4GA zSFDp2v=MFbjScr|w{RxvEX^3y84Ot{Fn?g*zPt9%-`jkOGe&Wir~}BvgqNj4(oP(} zu00PgnJgMKzHH*@r~;dYsEw}*<}1FLsmEZ+lF3JU2=IQb^O#1ju@$R)W^HsAy5A&n z;wAz1O+xpZa_Z%0pWW?|jaO1eIh86me`j#W30qWD3j@R9u%(WlIH8{IHkd2V-~wd3 zE2omZFY>i0m0u)2g*-C4y)pPqZZu|*EB+4S6!f((B3k+n@dnqwFjiDv4Z&{`?+hEB z6460S1lq4yxP<_Nh#bfR1Jfujl3k8X8a4DrwcO0Y&WvVU1^LD`Az^^{5|5%vAcFt~ zeZj9bJ7|YBe@&MEi{AQ9$uUtEbq%3fPBybejMA#1zYo@0c(`^rbFR)g5TzDzjtk}R zSmLCXzi&Bp9xt&eIgl+N%rD9IuXcN9JJ|$`g|O-^VyWVlF)r+6DSDA0=|%h=dLnQb zObH2sv&U70^eFMp=dG67B3?TTC6pGV%0RA0s&aZ?Zv+X089-Fy(r3o>b$F?3MCt3u zdTZeeKLCOheE&24x=WWf=T47Cz0!_*$3{177#+KJhgYilhTw?5zemt`ClhwD?VEn< z7(t{(c|Br*I!8sB5-KHdIRi5V{Pt(bewT?TgO}&AK#X&P(E8+;RpVNDll$9IyileJ z&oDM7_C(=lH-{IUBh)EE2HNPN#1zEf0f1yu{Q94XOCQ>NBNA(uz1snlRhtet24 zRX=xWYs^=>u2W-`pv&TY?U11I80Rl*<4+Jft`tK|dQ@&^ zc%YCQ4GoO0N!=Y>c&bBd&80@CLMnM$O*HR+b=wo_i6^#sGwhXAmDT#@Ht+S->J={6 zPn(UmPCog}Y#oYywQJuW@!e#5<>n8QZ#{$#ApYWrHD`_BZ5AX)gULezAw74S|tGo#wcjLq$>h4Q%(}9KGwo42z*-D35ps4r|x<2}5uT4&I{O~%#m7x;$p-<6>tTMtiDur}r#K#yH{&2?ao zW{Hm#tPq1SQAk`Nfu2|&>DtZHQxyIt)@L=q46RV-3{GNz*~EJFCco=d0y~3%adGRK z-VBRm+an6320>c~;B98X8;A6sC3qkyuET$4tEq);j%&IyE zq>t)z*zQDCxfy()DTE+Chfvz7LWPh*m<2YnMEZg%L-+{zf8(y6LjJ0;@hkFqY*dfr z^H+?n+fyv`++~h5XNBf78C*)9LuY#)*j=nh%g9 z1eulP;^~EHFcLh21VTPSLZDdGNhW2fD{O%bNvw4F2;KiUS;(>cNa-dC6AY#Vs}|V> zSlX&r>m*;HOwu@QwxKEJf4}kF~2xcz;b|1Y0)@lov|j6 zi|HGNF(VKyw68Nsz_|7R*vy?W57bpx{sQB{5Rd-Y5+__D1=Ycj{4WBa_cL zXaR$Vv8vGs!d`e2Q{V*<|EpALU6TMb3&_*2n8)>TMFCc}(XdWt^K^tlQ!2>jJ zufkrvnfaieAULn4c*@1@XdX<`ndt~pPfA(=^`pINseXqKA;g)}V<(1cl zn~mmY%e!i|YsQ*yL-J^~)ioxKdE>{8pF#Absd|ykge+k>m?a1-M{9_fA;v9O4y^X~ zKquyss6(N=j%~qVF&y}8>H75AC-~Z0eURW4gGXXRuFmhE;WILxl8hB`&deNY$a_TY zb#3$){h^GvMu3N7C0sJ{+VUWRzkCY28HU(`tH}X2xK`WO+$YlvNutGNfwO+&H~H%2 zDf*8XT=XF}6Yo@oj#zA2x`?|5iXbXVMz2wTh6L9e`6id-)N%OKCacMO?IZ>8gM6VZ zo9ySBOQ-9lavd8rW;UJPRQ43>B=%q^{TPf9uVdZ9GE33<@Eqe(Fxb3nRVr8Byiw00 zju0|6);-~B*o0AbB+uxe@Dfg_PVhQO$DgI$E(U}UB~svM6xez5Wr+TjZI#OQKF%P{ zIhd1WWL@SAulX!E2KfpAcws?^y3H5UL+SKj>htT@t9h|YQaVhObe_)Uc~8RiO2`b* zT1ZqCyow2@Qi=Fjp)i_Aq`(&}_1jiCm)|-tumfAc*j?zu9xa}l`F678!1x)CQp~wj zGffGSq1cg3WUH9Gkr$XlZ<=CHl!xy{jdwjy7-J)G+<2k44wJhK{ibv}{~B8|FyLa- zNb)2kG2Z;+ANebyxmG@0FaInmMuU%XPoOH|hkWqd-b{K!FFD=4L}xGO(rM#pWRwuuJUsbC12tsZUo8v-v_d zji8ywJD0GNN=ON>YGlJg>QjD0Zec8*Nbyihp04UlHd`p=3$ufTb?HpMOIrXjyT@9N zgu>|%y-mI75X1(oLoRp;u^Hz8_Yr+SgK#n7Yov`?pEiC=mDQBGRvlG$s7KUO>YM62 zKrLCyJ2Wd~_+Sw6MGa88pPZMrhvhq52rMCg2m-fD&|-!n`pDCsWl*2_U$%v?uTA?s zK_AMIA3q6-ShS2qIZg~+8@@a!$k|%UDk_MJPqG<%!2uc{+72IB|MZZ}c@7TX$8E~d zu6zq1LL&X*Jw1FW8A7yh&H7lkzyN+1Sq?Va78js63pDSe1qQ5Xzh2W`V;uoGW8twh z!Kn>ErFemL3|m@wld>$uZ3`V{n4lG0=ZS4%Wcm zrn*vz#Aty@lXbTi8(;ZB2`e=x?>j|~QIaW;KFxzm^)4tgkj-*RZJ=d2nJ8l|K|{($ z1;-p9RXLxVE>n6TdeaU3)hVeb-s=rH;#q!0IQG&gkr~UZ1vrr=u&+h3wPgvIh7k>i zJ_D;xdeG*r!-cN!{70Wp)Y9qU^!oM5{$!$>I9bbop&PmbM|3W34^dwBu+l5t@y%Vk zuDNE{u5aFP$2YFhCG|MD{pVcr3#3=5(bV8)zl+Wgz*yLA6`?KClYldXn}yHuvQ?s2 zUdL^>oK8&?7-0+9%yhpb_+tp>2@zKeAS>QdP12toc$Ix+3*)I&5gt5v5`!o^uDq7X z(eq6RZKXK#44#iz!F7>+l7FC@^kMrPxdNfKTgWSi7Md>;M3yHr=`zgo)MllQf+EoT zqXd^QV`0+@6qa?Cy5Zj{l&0VUGhv^CR0&S+@|xYCL|B5zs9RwCJ9 zT~jbMqmLisuxsRlG!0}T6s;+~?NqFrlVF2K)UosAm*Q}Yep{H^DaC2gk|A6k*6Cd= zq7*)n*X@1eI)>;O$Z{mOt|7}wb$(f{jgB5bYLi=g7y$EYN^WKs!43O&r2dh~fWfzr z6VK^ZpgbN)$$RjNab>xY%DQPMW91kEjx&`LCGrCI1qHA__->FdvLr&>fZs_rA~K&K zqo^+y3mchaa$T{wE|tuDGFEh43I>2*E@BFz@R~0s)R)EUw70oQQmZR3<`Vlk(3*;rNd&umBv8(M+H-0fa0$tT?B4 zOR2P_hh6{Kf1ksk_$>j2JT1-Uw;{tO`{PR!lX#;Vxe@c5KY*xsbr-#pqq%a+Gqbb% zKC`g!OCgawLcg@I@R@zHJHC92n)BTM>9LPnz1eJ|(mw+i1^DdurU+uK0cp`VJ2S%^R=VyE+aI321>cW0-}c25wIdW_j<5on z6?lfxtGYBkk$e^^#N)M0&w2@fTHg~p#IwR<`{mF0<@S+j+c-q$$1vXA@Ra4iKpRmxgOG1HVWaIZAJVkV9c`vJ2m2WImAQ) zD=6#)$QVec?m}gceCkcgR!w3zBKD|6VH%0Bxp-j5j$dDR=-)0}e>)UtG~>q?7XIzS z3%|Z&$AOhIyzh@djNvSq-NOOqs>lI7)1Mp04woYWbjkV(C?FX|s33SOP#dE*hneA~ zTkdY|4041Mbp^5g06~B@69S6jS`nKU$6Ld~EFZS8vJY11-!XomepY<}PUpX;UQz#1 z{WqYc2t2JlYORI0#UTmGx>St~b&)F}4*|w}awXY0S*~n}Zvt~Ixe{yo5`bm7a+N|Y zcZ(d$pHm%jCAnG(LZU!!mjo;H%gHXK+_EuU3ewul*_JTdek3|%f&}Ka-5&aAJ0Nup znG-Rn6p*TAPVz(Wq)v@%f^OW#T*qr1xT6B5-6T-1QzTH~Z$DCKs%fkZ8pFS|cki7$ zw{HFQqep*z>(-rj?%ngmu!6mb);i0d91Z~&a`=w8kUz1o6#5-Nl{m-S?yVeFjN*ISO8^R6PP5zXapfh z@F~PP#+IL{;H9;$q+a##lVe8(SxJkdWk|pH2e`u{AB**+L&$*XeBH{( zMlBdcawNxG1NM@QWeBI3WDm>e-NA7i8;#Iz!6qR+zQ9~IQ;Wx8?WY;Q(8EreKo)5- zIg(R|6GY+@%<;U2SVXKL!U9$Xx*id(1l!OE*l;hOu#P|}ps56xhk2iZb5QSB=*kjY zqKXdF4&A*VweTNOZU_(&td>f{u>_OoU=O-0dJEg9ohPrPj(i$5e(HYo;`q@|in!1Y zJmkiok{~?L0R8CjJ>aixSQlSGJ73}W(c9rs_one~C+Z61YV~tXz3@^|R?@U}fo4IL zQI?=K3D`(512&ZaL7PIO^({c^7Qk&8*@RybvRb$+nCWj9lA8PYM8B2mH>YOsul-V4 z-TXDRSJPm(GtU8yOV&NVw|D;f>*x3W-aVea^c_3IiOunt)04|@8_bW0$p+g{?UMOyEE3f|bhL7$Aa+XEhX?K zPOJ-jp!H}vdIi^J#CAZ&B&6ZZD=aHOEjAbgZ8;>A?M2WlA*b1Dr3XzUG%QdaZy7KNrU5CvlM!gt&LY_is}xoTs%(*$;IOlq9x4#abRf%&U(hkRC zjfqMJQSQBdVej7GyYB={8V3);q;bMCZkR87l;Bav%N~(?gs(D}Q14|ZedPtbYb;Eu z3dmWA?B7DI#3Z!jjUsvrU)?0H020xK-pOJQeiaKI@1b=-{MmA#len4b_Eo7PaxyoUtfmO{Soo~ zX0Bfm0_r!yYW!Ps*QaAKkQI`Bui9d^pY(-J-w)kA)wqsa5g_&i1G*2?N5!{9%op7t z-85kKrbuxZj^dEcU^- zg^>MNB>^f=LA`hT@-g#y=}3SR?FrP>47)!#SqGZQbL_wTc}!@%l1cLD2TY#EQjs}hprtN-d#dR3cc{w z-#JvgYGim}IEdGL^~6SKeYWHlB4N-qfL&WUN%$c-i5=odjG_fh9_1D#p@r8>s7;rB z(AgG@d*IjjpA^jfehkKht|p>2bu~Znb--K~a%C}~<|p+Z5xd)gm>Rh^bwTc_Hf}>< z1ZrVCro}$LX1MhM9D0k zvP}Xy1-=2ev1^y@1s&gs-04J)Hda+E8qQ}C+QcgkDYhaT5GT7f4X!2Fo8R|n*rV%b zS#gTP6Oj^Sz%op`Z0`xF?j9MrZU~-Vh(_;D)`L5Xh4RYVHf$QOIl%K;`-)&!0jVqq zv}sR}{`pax#4fh&V96nF+zlK-dOY@Z#`cU*) z{U2b#Vpf}8^()0&v1&5|1)UI-RqB>;fK8h4%7;a>rNRH5oLW<+7gK7Y(}__epSGI| z*-ClqNlP}=RZiE^N}!AjaLRl0ofSJZOO`v*oR|_-{omCgLJ$#}3i_^N?YN8gw6QN5 zvCS$+9Kn7ch=rOJ5G#=QNq`r^n3|a(HIG@vPBk2VFuR}x=E2yCVt0I+aXL-;TFjg9 zpEx}6b$z87IO4m#UD1=Cu z;Xp}+MHOGz+S~jxZ;J461YUpr3GNmQ%b~!$m$wB)boNI&Y93;NjKvgBbZPF!HnI~B zyslqL0`v5_K%8=BR@flA;Knf65bu#)2DVVF%3!GkMIA+YUo9*p=wsAfV8dyJ))?~3 zX+Io_?Rw1kb>lx8KU6tYSJ!m%l@9)}rWoA0F0t;sb?M7ehvTc(Wq-#?pWc3(NMHZH(1A6DZXp9tZNS@ze7LtzQix~CGY8ar@A zq;F2h1G2|I+L*j?)p})p0-TSS0MHUtGjo&|1l9x$m48FDL_ZO`32y?#ID$^hYICltsvugj3V~?w zL++0txI;{ns}3IY&U>d1c?Z3U9$i~kQgzmryWWltW7Ar!_jAG(RzBjvv#9eU3`@=GS|nps{Ta0Ia6 z+9hH7fCZF;xHiyy6)*23tE%<#@*~_?ZZ`tssQ)wqb3gh~(^^}+Z9Wpo!c1Vk%#Im6 z4rh^|D4ap42#v-eG9kc|Ht7H{GZ`@s%WfAbRxxKE-ve&vwDqSV^rt7Vg3cTN1My&+ z)IqEw*+tcRs$69{B+*We=WX9#x(Yue$D0vnf-W4dEd=I5O#UB=*}X5@ zx(r9_dWzo}+As}of5_W2TARw>Ju(R?&dG%-l9Lz}QZ+?E%v=;|Ms8x{51m|KYKCV` z!j(0l7MKdEyrZIE&Cf?ErrENZU!c7kv#QQk4r3)d!DiN5)yJp#Jz1cCS^<-<()N@e z4DJCKSiz?~BKK(g(5i#Hwh!`oZ<+JLION^L_Yk82)|}zZmlMBx&4K%Sd|NWr%PfI6 z_6d<=wPBGGiMw#Jk{xMu3ie)#nTm#m)Jw<~4U6GRJTM^socprmaVkZzVBF?&oL~w) z$0)sgc7^3y*{thf3_Q+$ku-Q%_OX3?u!)#tF!4)h=@I3q< z`Q<;m`^OVI{;Lr|(ii-kd+8_sP3!N)CM?oP?4XdQ$WG-b;T~)G;xZ+P!TEOrNv?@* zY5gEOr}sm*QO&FVKZfsf0Yka?A4Bc#zjggQ%2oV7qmM1=ze@gIyr{oF zi@$TaZ0Nsd@z-D1^Q+FkK|R2+H&-2R-QVr+AE2EHT#x6fCH((cl&A53aaA32_)h;V z{lE3y(AV*I9wpbv8hyrj^(*FP^MZBI`UXVCTLXU<9CCify(4ly`buIf^B39wmLJRi zQPC`It(5vU^k=Js)jt|6*1j`*VB{O)f%VH9zEl5Srk>pz-TG78uAWU_dGF5p&a-o$ znft3qC3F>COYX2zhF#NGr^^)7}RAK`D%CKI|Ai!gzA*C)G1}DQ_51Ol%-B7+qidF-z#-q2kXYQ z#u4OI_$k;p?lB%TegZw)WNbiYG(LCT{IyarZoox%89!~@Z`_89<;How;S;#!E}Z=d muI4tFtUr#>=c;kL@hN=nN4VL85E~!lYd6riPz>H>e*SMCr5IBH literal 26600 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 N{33=BW8vS~{tq4+)o1_! From 6863dd75efacc20ba0dde7eb6c9e94a43c4c5a30 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 19 Dec 2014 17:05:13 +1100 Subject: [PATCH 443/681] Restyling button for price breakdown to make it more clear that users press again to close the pop over --- .../price_breakdown_button.html.haml | 2 +- .../darkswarm/_shop-popovers.css.sass | 56 ++++++++----------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/templates/price_breakdown_button.html.haml b/app/assets/javascripts/templates/price_breakdown_button.html.haml index e5dbbe7123..5eff1ef237 100644 --- a/app/assets/javascripts/templates/price_breakdown_button.html.haml +++ b/app/assets/javascripts/templates/price_breakdown_button.html.haml @@ -1,2 +1,2 @@ %button.graph-button{"ng-class" => "{open: tt_isOpen}"} - %i.ofn-i_058-graph + / %i.ofn-i_058-graph diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index feb6529fa2..b50586ab6e 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -77,44 +77,36 @@ ordercycle button.graph-button z-index: 9999999 border: 1px solid transparent - @include box-shadow(none) padding: 0 margin: 0 - @include border-radius(999rem) display: inline background-color: rgba(255,255,255,0.5) - padding: 5px - @media all and (max-width: 768px) - display: none - // Hide for small - - &:hover, &:active, &:focus - background-color: rgba(255,255,255,1) - i.ofn-i_058-graph - color: $clr-brick-bright - - i.ofn-i_058-graph + padding: 4px + @include box-shadow(none) + @include border-radius(999rem) + &:before + @include icon-font + content: '\e639' color: #999 - margin: 0 - padding: 0 - font-size: 1rem - - @media all and (max-width: 640px) - padding: 3px - i.ofn-i_058-graph - font-size: 0.75rem + &:focus + border: 1px solid #e0e0e0 + background-color: rgba(255,255,255,1) + &:before + color: #666 + &:hover, &:active + background-color: rgba(255,255,255,1) + border: 1px solid transparent + &:before + color: $clr-brick-bright + @media all and (max-width: 768px) + // Hide for small + display: none button.graph-button.open - @include box-shadow(inset 0 1px 1px 0 rgba(0,0,0,0.35)) - border: 1px solid #999 - - &:hover, &:active, &:focus - background-color: rgba(255,255,255,1) - i.ofn-i_058-graph - color: $clr-brick-bright - - i.ofn-i_058-graph - color: $clr-brick - + border: 1px solid transparent + background-color: $clr-brick-bright + &:before + content: '\e608' + color: white From 586753015b2fe309cc03fbdcdd53f9248c7a4fa1 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 18 Dec 2014 10:25:01 +1100 Subject: [PATCH 444/681] Reducing the groups page to a oneline list of groups --- .../stylesheets/darkswarm/groups.css.sass | 40 +++++-------------- app/views/groups/index.html.haml | 38 ++++-------------- 2 files changed, 16 insertions(+), 62 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index b859273ab7..4686c452a2 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -9,34 +9,12 @@ padding-bottom: 20px .group - padding-bottom: 40px - hr - border-bottom: 10px solid white - outline: 0 - border-top: 0 - margin: 0 - -.group-hero - position: relative - padding: 0 - border: 10px solid white - background: white - -h3.group-name - margin-top: 0.5em - margin-bottom: 0.15em - -img.group-logo - max-width: 220px - max-height: 86px - float: right - padding-top: 10px - - -img.group-hero-img - background-color: black - width: 100% - height: inherit - max-height: 260px - min-height: 120px - overflow: hidden \ No newline at end of file + padding-bottom: 0.5em + .row div + font-size: 110% + .row a + font-weight: 500 + vertical-align: middle + .ofn-i_035-groups + font-size: 120% + vertical-align: middle diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 1aa5b02629..b085bb446e 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -5,50 +5,26 @@ #groups{"ng-controller" => "GroupsCtrl"} #active-table-search.row.pad-top - .small-12.columns.text-center - %h1 Groups / Regions - %div - Check out our - %ofn-modal{title: "food groups"} - = render partial: "modals/groups" - below + .small-12.columns + %h1 Groups / regions %p %input.animate-show{type: :text, "ng-model" => "query", - placeholder: "Search group name", + placeholder: "Search name or keyword", "ng-debounce" => "150", "ofn-disable-enter" => true} .group{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", name: "group{{group.id}}", id: "group{{group.id}}"} - .row.pad-top{bindonce: true} - .small-12.columns - .group-hero - %img.group-hero-img{"bo-src" => "group.promo_image"} - %img.group-logo{"bo-src" => "group.logo", "bo-if" => "group.logo"} - %h3.group-name - %i.ofn-i_035-groups - {{ group.name }} - %h5.group-description {{ group.description }} - .row.pad-top{bindonce: true} .small-6.columns - %p {{ group.long_description }} + %a{"ng-href" => "group/{{group.id}}"} + %i.ofn-i_035-groups + {{ group.name }} .small-6.columns - %h5 Our hubs & producers - %ul.small-block-grid-2 - %li{"ng-repeat" => "enterprise in group.enterprises", "scroll-after-load" => true} - %enterprise-modal{"ng-if" => "enterprise.is_distributor"} - {{ enterprise.name }} - %enterprise-modal{"ng-if" => "!enterprise.is_distributor", "show-hub-actions" => 'true'} - {{ enterprise.name }} - - - .row.group_footer - .small-12.columns - %hr + {{ group.description }} .group{"ng-show" => "groups.length == 0"} .row.pad-top From 9798b05a24c90e5bb6ab05138fc192acc4f53408 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 18 Dec 2014 15:42:56 +1100 Subject: [PATCH 445/681] TabsCtrl can show tabs that don't toggle toggle: tabs show on first click and hide on second click select: tabs show on every click, one tab is always active --- .../darkswarm/controllers/tabs_controller.js.coffee | 8 ++++++-- app/views/shopping_shared/_tabs.html.haml | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee index 0bbe0bbd23..74ce167580 100644 --- a/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee @@ -1,10 +1,14 @@ -Darkswarm.controller "TabsCtrl", ($scope, $rootScope, $location, OrderCycle) -> +Darkswarm.controller "TabsCtrl", ($scope, $rootScope, $location) -> # Return active if supplied path matches url hash path. $scope.active = (path)-> $location.hash() == path + # Select tab by setting the url hash path. + $scope.select= (path)-> + $location.hash path + # Toggle tab selected status by setting the url hash path. - $scope.select = (path)-> + $scope.toggle = (path)-> if $scope.active(path) $location.hash "" else diff --git a/app/views/shopping_shared/_tabs.html.haml b/app/views/shopping_shared/_tabs.html.haml index d136efa045..90ecc663ea 100644 --- a/app/views/shopping_shared/_tabs.html.haml +++ b/app/views/shopping_shared/_tabs.html.haml @@ -11,6 +11,6 @@ %tab.columns{heading: heading, id: "tab_#{name}", active: "active(\'#{name}\')", - select: "select(\'#{name}\')", + select: "toggle(\'#{name}\')", class: "small-12 medium-#{cols}" } = render "shopping_shared/#{name}" From 9310bc902a26b0914ca775627ad1aae87a616247 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 18 Dec 2014 16:00:54 +1100 Subject: [PATCH 446/681] first draft of group page, a lot of TODOs --- app/controllers/groups_controller.rb | 4 +++ app/views/groups/show.html.haml | 42 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 app/views/groups/show.html.haml diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 504bfa9569..8653131b5a 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -5,4 +5,8 @@ class GroupsController < BaseController def index @groups = EnterpriseGroup.on_front_page.by_position end + + def show + @group = EnterpriseGroup.find params[:id] + end end diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml new file mode 100644 index 0000000000..1d0cf8b21b --- /dev/null +++ b/app/views/groups/show.html.haml @@ -0,0 +1,42 @@ +%div{style: "padding: 1.5em;"} + %img{"src" => @group.promo_image, style: "display: block"} + %div{style: "margin-bottom: 1em"} + %img{"src" => @group.logo} + %div{style: "display: inline-block; vertical-align: middle"} + %h2{style: "margin-bottom: 0em"}= @group.name + %div= @group.description + + #div{"ng-controller" => "TabsCtrl", style: "clear: both"} + %tabset + %tab{heading: 'Map', + active: "active(\'\')", + select: "select(\'\')"} + %p TODO: show a map of enterprises + %tab{heading: 'About us', + active: "active(\'hi\')", + select: "select(\'hi\')"} + %h3 About us + %p= @group.long_description + %tab{heading: 'Our producers', + active: "active(\'producers\')", + select: "select(\'producers\')"} + %h3 Our producers + %ul + - @group.enterprises.is_primary_producer.each do |enterprise| + %li= link_to enterprise.name, enterprise + %tab{heading: 'Our hubs', + active: "active(\'hubs\')", + select: "select(\'hubs\')"} + %h3 Our producers + %ul + - @group.enterprises.is_distributor.each do |enterprise| + %li= link_to enterprise.name, enterprise + %tab{heading: 'Contact us', + active: "active(\'contact\')", + select: "select(\'contact\')"} + %h3 Contact us + %p TODO: provide contact information + + %p TODO: show the group's contact footer + += render partial: "shared/footer" From 9956b967f0e59a1e184e9f462c48ddea65834852 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 7 Jan 2015 10:27:08 +1100 Subject: [PATCH 447/681] Fixing link to group's page --- app/views/groups/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index b085bb446e..d9297d7202 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -20,7 +20,7 @@ id: "group{{group.id}}"} .row.pad-top{bindonce: true} .small-6.columns - %a{"ng-href" => "group/{{group.id}}"} + %a{"ng-href" => "groups/{{group.id}}"} %i.ofn-i_035-groups {{ group.name }} .small-6.columns From 3b9657eb1726ab221de55838418253a6f0d5c0d7 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 7 Jan 2015 12:00:55 +1100 Subject: [PATCH 448/681] show map of a group's enterprises --- app/views/groups/show.html.haml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 1d0cf8b21b..0d5fe29359 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -12,6 +12,12 @@ active: "active(\'\')", select: "select(\'\')"} %p TODO: show a map of enterprises + = inject_json_ams "enterprises", @group.enterprises, Api::EnterpriseSerializer + .map-container + %map{"ng-controller" => "MapCtrl"} + %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} + %markers{models: "OfnMap.enterprises", fit: "true", + coords: "'self'", icon: "'icon'", click: "'reveal'"} %tab{heading: 'About us', active: "active(\'hi\')", select: "select(\'hi\')"} From 370133b875070ed219152cdc2af85a4e8f59946c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 7 Jan 2015 16:06:23 +1100 Subject: [PATCH 449/681] Fix minor spec issues --- spec/features/consumer/shopping/variant_overrides_spec.rb | 2 +- spec/support/request/shop_workflow.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index 8aec1079ea..4e34fde117 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -75,7 +75,7 @@ feature "shopping with variant overrides defined", js: true do page.should have_selector "tr.line-item.variant-#{v1.id} .cart-item-price", text: '$61.11' page.should have_field "order[line_items_attributes][0][quantity]", with: '2' - page.should have_selector "tr.line-item.variant-#{v1.id} .cart-item-total", text: '$122.21' + page.should have_selector "tr.line-item.variant-#{v1.id} .cart-item-total", text: '$122.22' page.should have_selector "#edit-cart .item-total", text: '$122.21' page.should have_selector "#edit-cart .grand-total", text: '$122.21' diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index 505fccb03c..8f088095e8 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -1,6 +1,6 @@ module ShopWorkflow def add_to_cart - wait_until_button_enabled 'input.add_to_cart' + wait_until_enabled 'input.add_to_cart' first("input.add_to_cart:not([disabled='disabled'])").click end From c0030ddb134748050243f52801d392cc8c5742ff Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 7 Jan 2015 16:28:03 +1100 Subject: [PATCH 450/681] use_short_wait can now take a flexible wait time --- spec/features/consumer/shopping/variant_overrides_spec.rb | 2 +- spec/support/request/web_helper.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index 4e34fde117..abe11de532 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -7,7 +7,7 @@ feature "shopping with variant overrides defined", js: true do include CheckoutWorkflow include UIComponentHelper - use_short_wait + use_short_wait 10 describe "viewing products" do let(:hub) { create(:distributor_enterprise, with_payment_and_shipping: true) } diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index a77f12d7dd..db8b3af51c 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -11,8 +11,8 @@ module WebHelper # use_short_wait # ... # end - def use_short_wait - around { |example| Capybara.using_wait_time(2) { example.run } } + def use_short_wait(seconds=2) + around { |example| Capybara.using_wait_time(seconds) { example.run } } end end From 4bf87167866f22a7516fa59bc4ed9ceda369f49f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 7 Jan 2015 16:29:15 +1100 Subject: [PATCH 451/681] Extract complete_checkout to method --- .../shopping/variant_overrides_spec.rb | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index abe11de532..4b73d0b4df 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -98,37 +98,8 @@ feature "shopping with variant overrides defined", js: true do wait_until_enabled 'li.cart a.button' click_link 'Quick checkout' - checkout_as_guest + complete_checkout - within "#details" do - fill_in "First Name", with: "Some" - fill_in "Last Name", with: "One" - fill_in "Email", with: "test@example.com" - fill_in "Phone", with: "0456789012" - end - - toggle_billing - within "#billing" do - fill_in "Address", with: "123 Street" - select "Australia", from: "Country" - select "Victoria", from: "State" - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" - end - - toggle_shipping - within "#shipping" do - choose sm.name - end - - toggle_payment - within "#payment" do - choose pm.name - end - - ActionMailer::Base.deliveries.length.should == 0 - place_order - page.should have_content "Your order has been processed successfully" ActionMailer::Base.deliveries.length.should == 2 email = ActionMailer::Base.deliveries.last @@ -142,4 +113,43 @@ feature "shopping with variant overrides defined", js: true do it "subtracts stock from the override" end + + + private + + def complete_checkout + checkout_as_guest + + within "#details" do + fill_in "First Name", with: "Some" + fill_in "Last Name", with: "One" + fill_in "Email", with: "test@example.com" + fill_in "Phone", with: "0456789012" + end + + toggle_billing + within "#billing" do + fill_in "Address", with: "123 Street" + select "Australia", from: "Country" + select "Victoria", from: "State" + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" + end + + toggle_shipping + within "#shipping" do + choose sm.name + end + + toggle_payment + within "#payment" do + choose pm.name + end + + place_order + #sleep 5 + using_wait_time 10 do + page.should have_content "Your order has been processed successfully" + end + end end From f90ee33c8936b05266117bf23f90abc7df6874c6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 09:27:29 +1100 Subject: [PATCH 452/681] Use the tax category factory provided by Spree --- spec/factories.rb | 5 ----- spec/features/admin/products_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index 2e3c8e223a..214c61962d 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -180,11 +180,6 @@ FactoryGirl.define do order.reload end end - - factory :simple_tax_category, :class => Spree::TaxCategory do - name "Test Tax Category" - description "Test tax category description" - end end diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index c42a57ce98..f3141b765f 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -15,8 +15,8 @@ feature %q{ end describe "creating a product" do - scenario "assigning a important attributes", js: true do - tax_category = create(:simple_tax_category) + scenario "assigning important attributes", js: true do + tax_category = create(:tax_category, name: 'Test Tax Category') login_to_admin_section click_link 'Products' From a9b91bc52a7d8778b2f4a8e8ea54658842054b45 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 09:35:18 +1100 Subject: [PATCH 453/681] Tighten spec: setting tax category should succeed --- spec/features/admin/products_spec.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index f3141b765f..0e27930f6e 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -6,6 +6,8 @@ feature %q{ } do include AuthenticationWorkflow include WebHelper + + let!(:taxon) { create(:taxon) } background do @@ -15,8 +17,9 @@ feature %q{ end describe "creating a product" do + let!(:tax_category) { create(:tax_category, name: 'Test Tax Category') } + scenario "assigning important attributes", js: true do - tax_category = create(:tax_category, name: 'Test Tax Category') login_to_admin_section click_link 'Products' @@ -88,6 +91,7 @@ feature %q{ end context "as an enterprise user" do + let!(:tax_category) { create(:tax_category) } before do @new_user = create_enterprise_user @@ -120,7 +124,7 @@ feature %q{ page.should have_selector('#product_supplier_id') select 'Another Supplier', :from => 'product_supplier_id' select taxon.name, from: "product_primary_taxon_id" - page.should have_select 'product_tax_category_id' if Spree::TaxCategory.any? + select tax_category.name, from: "product_tax_category_id" # Should only have suppliers listed which the user can manage page.should have_select 'product_supplier_id', with_options: [@supplier2.name, @supplier_permitted.name] @@ -131,6 +135,7 @@ feature %q{ flash_message.should == 'Product "A new product !!!" has been successfully created!' product = Spree::Product.find_by_name('A new product !!!') product.supplier.should == @supplier2 + product.tax_category.should == tax_category end scenario "editing a product" do @@ -138,12 +143,13 @@ feature %q{ visit spree.edit_admin_product_path product - page.should have_select 'product_tax_category_id' select 'Permitted Supplier', from: 'product_supplier_id' + select tax_category.name, from: 'product_tax_category_id' click_button 'Update' flash_message.should == 'Product "a product" has been successfully updated!' product.reload product.supplier.should == @supplier_permitted + product.tax_category.should == tax_category end scenario "editing product distributions" do From e4efda2f961e6f5e1201052f1a617e4dbb804526 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 09:37:35 +1100 Subject: [PATCH 454/681] Move model spec into validations block --- spec/models/spree/product_spec.rb | 45 ++++++++++++++++--------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 2a399ba30e..3e4999556d 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -9,28 +9,6 @@ module Spree it { should have_many(:product_distributions) } end - describe "validating tax category" do - context "when a tax category is required" do - before { Spree::Config.products_require_tax_category = true } - - it "is invalid when a tax category is not provided" do - product = create(:product) - product.tax_category_id = nil - product.should_not be_valid - end - end - - context "when a tax category is not required" do - before { Spree::Config.products_require_tax_category = false } - - it "is valid when a tax category is not provided" do - product = create(:product) - product.tax_category_id = nil - product.should be_valid - end - end - end - describe "validations and defaults" do it "is valid when created from factory" do create(:product).should be_valid @@ -63,6 +41,29 @@ module Spree product.available_on.should == Time.now end + describe "tax category" do + context "when a tax category is required" do + before { Spree::Config.products_require_tax_category = true } + + it "is invalid when a tax category is not provided" do + product = create(:product) + product.tax_category_id = nil + product.should_not be_valid + end + end + + context "when a tax category is not required" do + before { Spree::Config.products_require_tax_category = false } + + it "is valid when a tax category is not provided" do + product = create(:product) + product.tax_category_id = nil + product.should be_valid + end + end + end + + context "when the product has variants" do let(:product) do product = create(:simple_product) From ac59665e3cb2164fe745b6cd020a36de1816792d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 09:48:33 +1100 Subject: [PATCH 455/681] Test validations without creating models in database --- spec/models/spree/product_spec.rb | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 3e4999556d..3b3f5ec0a0 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -10,21 +10,16 @@ module Spree end describe "validations and defaults" do - it "is valid when created from factory" do - create(:product).should be_valid + it "is valid when built from factory" do + build(:product).should be_valid end it "requires a primary taxon" do - product = create(:simple_product) - product.taxons = [] - product.primary_taxon = nil - product.should_not be_valid + build(:simple_product, taxons: [], primary_taxon: nil).should_not be_valid end it "requires a supplier" do - product = create(:simple_product) - product.supplier = nil - product.should_not be_valid + build(:simple_product, supplier: nil).should_not be_valid end it "does not save when master is invalid" do @@ -46,9 +41,7 @@ module Spree before { Spree::Config.products_require_tax_category = true } it "is invalid when a tax category is not provided" do - product = create(:product) - product.tax_category_id = nil - product.should_not be_valid + build(:product, tax_category_id: nil).should_not be_valid end end @@ -56,9 +49,7 @@ module Spree before { Spree::Config.products_require_tax_category = false } it "is valid when a tax category is not provided" do - product = create(:product) - product.tax_category_id = nil - product.should be_valid + build(:product, tax_category_id: nil).should be_valid end end end From 4839c00d628290d887dc99321a8049fffba9cf00 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 10:12:27 +1100 Subject: [PATCH 456/681] Retrieve currency_symbol via private method rather than before_filter + instance var - cleaner syntax --- .../spree/admin/reports_controller_decorator.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 7105ce7734..4e5091d364 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -31,7 +31,6 @@ Spree::Admin::ReportsController.class_eval do # Fetches user's distributors, suppliers and order_cycles before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management] - before_filter :set_currency_symbol # Render a partial for orders and fulfillment description respond_override :index => { :html => { :success => lambda { @@ -281,7 +280,7 @@ Spree::Admin::ReportsController.class_eval do when "payments_by_payment_type" table_items = payments - header = ["Payment State", "Distributor", "Payment Type", "Total (#{@currency_symbol})"] + header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] columns = [ proc { |payments| payments.first.order.payment_state }, proc { |payments| payments.first.order.distributor.name }, @@ -298,7 +297,7 @@ Spree::Admin::ReportsController.class_eval do when "itemised_payment_totals" table_items = orders - header = ["Payment State", "Distributor", "Product Total (#{@currency_symbol})", "Shipping Total (#{@currency_symbol})", "Outstanding Balance (#{@currency_symbol})", "Total (#{@currency_symbol})"] + header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})", "Total (#{currency_symbol})"] columns = [ proc { |orders| orders.first.payment_state }, proc { |orders| orders.first.distributor.name }, @@ -315,7 +314,7 @@ Spree::Admin::ReportsController.class_eval do when "payment_totals" table_items = orders - header = ["Payment State", "Distributor", "Product Total (#{@currency_symbol})", "Shipping Total (#{@currency_symbol})", "Total (#{@currency_symbol})", "EFT (#{@currency_symbol})", "PayPal (#{@currency_symbol})", "Outstanding Balance (#{@currency_symbol})"] + header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Total (#{currency_symbol})", "EFT (#{currency_symbol})", "PayPal (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})"] columns = [ proc { |orders| orders.first.payment_state }, proc { |orders| orders.first.distributor.name }, @@ -334,7 +333,7 @@ Spree::Admin::ReportsController.class_eval do else table_items = payments - header = ["Payment State", "Distributor", "Payment Type", "Total (#{@currency_symbol})"] + header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] columns = [ proc { |payments| payments.first.order.payment_state }, proc { |payments| payments.first.order.distributor.name }, @@ -494,7 +493,7 @@ Spree::Admin::ReportsController.class_eval do table_items = @line_items @include_blank = 'All' - header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item (#{@currency_symbol})", "Item + Fees (#{@currency_symbol})", "Dist (#{@currency_symbol})", "Ship (#{@currency_symbol})", "Total (#{@currency_symbol})", "Paid?", + header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Dist (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", "Shipping", "Delivery?", "Ship street", "Ship street 2", "Ship city", "Ship postcode", "Ship state", "Order notes"] rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address } @@ -618,8 +617,8 @@ Spree::Admin::ReportsController.class_eval do private - def set_currency_symbol - @currency_symbol = Spree::Money.currency_symbol + def currency_symbol + Spree::Money.currency_symbol end def load_data From b6f29c778ecc92000b277ad87bbe8ef4b8836a79 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 10:23:37 +1100 Subject: [PATCH 457/681] Use haml syntax for defining divs --- app/views/spree/admin/reports/bulk_coop.html.haml | 8 ++++---- app/views/spree/admin/reports/customers.html.haml | 8 ++++---- .../spree/admin/reports/orders_and_distributors.html.haml | 4 ++-- app/views/spree/admin/reports/payments.html.haml | 8 ++++---- .../spree/admin/reports/products_and_inventory.html.haml | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/views/spree/admin/reports/bulk_coop.html.haml b/app/views/spree/admin/reports/bulk_coop.html.haml index c405b78526..c9178d89db 100644 --- a/app/views/spree/admin/reports/bulk_coop.html.haml +++ b/app/views/spree/admin/reports/bulk_coop.html.haml @@ -2,17 +2,17 @@ = label_tag nil, t(:date_range) %br .date-range-filter - %div{"class" => "left sub-field"} + .left.sub-field = f.text_field :completed_at_gt, :class => 'datepicker' %br = label_tag nil, t(:start), :class => 'sub' - %div{"class" => "right sub-field"} + .right.sub-field = f.text_field :completed_at_lt, :class => 'datepicker' %br = label_tag nil, t(:stop) %br - %div{"class" => "row"} - %div{"class" => "four columns alpha"} + .row + .four.columns.alpha = label_tag nil, "Distributor: " = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) = label_tag nil, "Report Type: " diff --git a/app/views/spree/admin/reports/customers.html.haml b/app/views/spree/admin/reports/customers.html.haml index db5fc94ce6..aef51702a6 100644 --- a/app/views/spree/admin/reports/customers.html.haml +++ b/app/views/spree/admin/reports/customers.html.haml @@ -1,19 +1,19 @@ = form_tag spree.customers_admin_reports_url do |f| %br - %div{"class" => "row"} - %div{"class" => "four columns alpha"} + .row + .four.columns.alpha = label_tag nil, "Distributor: " = select_tag(:distributor_id, options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), {:include_blank => true, :class => "select2 fullwidth"}) - %div{"class" => "four columns"} + .four.columns = label_tag nil, "Supplier: " = select_tag(:supplier_id, options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), {:include_blank => true, :class => "select2 fullwidth"}) - %div{"class" => "six columns"} + .six.columns = label_tag nil, "Order Cycle: " = select_tag(:order_cycle_id, options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), diff --git a/app/views/spree/admin/reports/orders_and_distributors.html.haml b/app/views/spree/admin/reports/orders_and_distributors.html.haml index 26137158b0..05e07243b3 100644 --- a/app/views/spree/admin/reports/orders_and_distributors.html.haml +++ b/app/views/spree/admin/reports/orders_and_distributors.html.haml @@ -2,11 +2,11 @@ = label_tag nil, t(:date_range) %br .date-range-filter - %div{"class" => "left sub-field"} + .left.sub-field = s.text_field :completed_at_gt, :class => 'datepicker' %br = label_tag nil, t(:start), :class => 'sub' - %div{"class" => "right sub-field"} + .right.sub-field = s.text_field :completed_at_lt, :class => 'datepicker' %br = label_tag nil, t(:stop) diff --git a/app/views/spree/admin/reports/payments.html.haml b/app/views/spree/admin/reports/payments.html.haml index 9f145e2858..26c523e87a 100644 --- a/app/views/spree/admin/reports/payments.html.haml +++ b/app/views/spree/admin/reports/payments.html.haml @@ -2,17 +2,17 @@ = label_tag nil, t(:date_range) %br .date-range-filter - %div{"class" => "left sub-field"} + .left.sub-field = f.text_field :completed_at_gt, :class => 'datepicker' %br = label_tag nil, t(:start), :class => 'sub' - %div{"class" => "right sub-field"} + .right.sub-field = f.text_field :completed_at_lt, :class => 'datepicker' %br = label_tag nil, t(:stop) %br - %div{"class" => "row"} - %div{"class" => "four columns alpha"} + .row + .four.columns.alpha = label_tag nil, "Distributor: " = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) = label_tag nil, "Report Type: " diff --git a/app/views/spree/admin/reports/products_and_inventory.html.haml b/app/views/spree/admin/reports/products_and_inventory.html.haml index 22bd58c29f..c3470e6047 100644 --- a/app/views/spree/admin/reports/products_and_inventory.html.haml +++ b/app/views/spree/admin/reports/products_and_inventory.html.haml @@ -1,21 +1,21 @@ = form_tag spree.products_and_inventory_admin_reports_url do |f| %br - %div{"class" => "row"} - %div{"class" => "four columns alpha"} + .row + .four.columns.alpha = label_tag nil, "Distributor: " = select_tag(:distributor_id, options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), {:include_blank => true, :class => "select2 fullwidth"}) - %div{"class" => "four columns"} + .four.columns = label_tag nil, "Supplier: " = select_tag(:supplier_id, options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), {:include_blank => true, :class => "select2 fullwidth"}) - %div{"class" => "six columns"} + .six.columns = label_tag nil, "Order Cycle: " = select_tag(:order_cycle_id, options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), From 6adb4194c2dcbdf2ee9b81522323128afbf5a2ec Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 10:31:51 +1100 Subject: [PATCH 458/681] Extract date range form to partial --- .../admin/reports/_date_range_form.html.haml | 9 +++++++++ .../spree/admin/reports/bulk_coop.html.haml | 14 ++------------ .../reports/orders_and_distributors.html.haml | 16 ++++------------ .../reports/orders_and_fulfillment.html.haml | 10 +--------- app/views/spree/admin/reports/payments.html.haml | 14 ++------------ 5 files changed, 18 insertions(+), 45 deletions(-) create mode 100644 app/views/spree/admin/reports/_date_range_form.html.haml diff --git a/app/views/spree/admin/reports/_date_range_form.html.haml b/app/views/spree/admin/reports/_date_range_form.html.haml new file mode 100644 index 0000000000..4d76b95d9f --- /dev/null +++ b/app/views/spree/admin/reports/_date_range_form.html.haml @@ -0,0 +1,9 @@ +.row.date-range-filter + = label_tag nil, t(:date_range) + %br + = label_tag nil, t(:start), :class => 'inline' + = f.text_field :completed_at_gt, :class => 'datetimepicker datepicker-from' + %span.range-divider + %i.icon-arrow-right + = f.text_field :completed_at_lt, :class => 'datetimepicker datepicker-to' + = label_tag nil, t(:end), :class => 'inline' diff --git a/app/views/spree/admin/reports/bulk_coop.html.haml b/app/views/spree/admin/reports/bulk_coop.html.haml index c9178d89db..d80a449bb4 100644 --- a/app/views/spree/admin/reports/bulk_coop.html.haml +++ b/app/views/spree/admin/reports/bulk_coop.html.haml @@ -1,16 +1,6 @@ = form_for @search, :url => spree.bulk_coop_admin_reports_path do |f| - = label_tag nil, t(:date_range) - %br - .date-range-filter - .left.sub-field - = f.text_field :completed_at_gt, :class => 'datepicker' - %br - = label_tag nil, t(:start), :class => 'sub' - .right.sub-field - = f.text_field :completed_at_lt, :class => 'datepicker' - %br - = label_tag nil, t(:stop) - %br + = render 'date_range_form', f: f + .row .four.columns.alpha = label_tag nil, "Distributor: " diff --git a/app/views/spree/admin/reports/orders_and_distributors.html.haml b/app/views/spree/admin/reports/orders_and_distributors.html.haml index 05e07243b3..23baad395e 100644 --- a/app/views/spree/admin/reports/orders_and_distributors.html.haml +++ b/app/views/spree/admin/reports/orders_and_distributors.html.haml @@ -1,19 +1,11 @@ -= form_for @search, :url => spree.orders_and_distributors_admin_reports_path do |s| - = label_tag nil, t(:date_range) - %br - .date-range-filter - .left.sub-field - = s.text_field :completed_at_gt, :class => 'datepicker' - %br - = label_tag nil, t(:start), :class => 'sub' - .right.sub-field - = s.text_field :completed_at_lt, :class => 'datepicker' - %br - = label_tag nil, t(:stop) += form_for @search, :url => spree.orders_and_distributors_admin_reports_path do |f| + = render 'date_range_form', f: f + = check_box_tag :csv = label_tag :csv, "Download as csv" %br = button t(:search) + %br %br %table#listing_orders.index diff --git a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml index 522fdfdf39..081cb43384 100644 --- a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml +++ b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml @@ -1,13 +1,5 @@ = form_for @search, :url => spree.orders_and_fulfillment_admin_reports_path do |f| - .row.date-range-filter - = label_tag nil, t(:date_range) - %br - = label_tag nil, t(:start), :class => 'inline' - = f.text_field :completed_at_gt, :class => 'datetimepicker datepicker-from' - %span.range-divider - %i.icon-arrow-right - = f.text_field :completed_at_lt, :class => 'datetimepicker datepicker-to' - = label_tag nil, t(:end), :class => 'inline' + = render 'date_range_form', f: f .row .alpha.two.columns= label_tag nil, "Hubs: " diff --git a/app/views/spree/admin/reports/payments.html.haml b/app/views/spree/admin/reports/payments.html.haml index 26c523e87a..1a92d507db 100644 --- a/app/views/spree/admin/reports/payments.html.haml +++ b/app/views/spree/admin/reports/payments.html.haml @@ -1,16 +1,6 @@ = form_for @search, :url => spree.payments_admin_reports_path do |f| - = label_tag nil, t(:date_range) - %br - .date-range-filter - .left.sub-field - = f.text_field :completed_at_gt, :class => 'datepicker' - %br - = label_tag nil, t(:start), :class => 'sub' - .right.sub-field - = f.text_field :completed_at_lt, :class => 'datepicker' - %br - = label_tag nil, t(:stop) - %br + = render 'date_range_form', f: f + .row .four.columns.alpha = label_tag nil, "Distributor: " From ab26902e4e661738d45a54b2d038dc54695457e4 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 10:39:14 +1100 Subject: [PATCH 459/681] Move money decorator to lib dir, as it's a decorator for a lib class, not a model --- app/controllers/spree/admin/reports_controller_decorator.rb | 1 + {app/models => lib}/spree/money_decorator.rb | 0 2 files changed, 1 insertion(+) rename {app/models => lib}/spree/money_decorator.rb (100%) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 4e5091d364..2be9876933 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -6,6 +6,7 @@ require 'open_food_network/order_grouper' require 'open_food_network/customers_report' require 'open_food_network/users_and_enterprises_report' require 'open_food_network/order_cycle_management_report' +require 'spree/money_decorator' Spree::Admin::ReportsController.class_eval do diff --git a/app/models/spree/money_decorator.rb b/lib/spree/money_decorator.rb similarity index 100% rename from app/models/spree/money_decorator.rb rename to lib/spree/money_decorator.rb From d2e0d4f44a99b43d68075d0626986dbea9e2c5a3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 10:39:51 +1100 Subject: [PATCH 460/681] Fix grammar --- lib/spree/money_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/spree/money_decorator.rb b/lib/spree/money_decorator.rb index e179343bc5..fed92b8210 100644 --- a/lib/spree/money_decorator.rb +++ b/lib/spree/money_decorator.rb @@ -1,6 +1,6 @@ Spree::Money.class_eval do - # return the currency symbol (on it's own) for the current default currency + # return the currency symbol (on its own) for the current default currency def self.currency_symbol Money.new(0, Spree::Config[:currency]).symbol end From 51b0d7e0eb01829a88e5dc0c0fd915ec673bf244 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 10:46:03 +1100 Subject: [PATCH 461/681] Move private method to helper --- .../spree/admin/reports_controller_decorator.rb | 7 ++----- app/helpers/spree/reports_helper.rb | 5 +++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 2be9876933..861d6a2b16 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -6,10 +6,11 @@ require 'open_food_network/order_grouper' require 'open_food_network/customers_report' require 'open_food_network/users_and_enterprises_report' require 'open_food_network/order_cycle_management_report' -require 'spree/money_decorator' Spree::Admin::ReportsController.class_eval do + include Spree::ReportsHelper + REPORT_TYPES = { orders_and_fulfillment: [ ['Order Cycle Supplier Totals',:order_cycle_supplier_totals], @@ -617,10 +618,6 @@ Spree::Admin::ReportsController.class_eval do end private - - def currency_symbol - Spree::Money.currency_symbol - end def load_data # Load distributors either owned by the user or selling their enterprises products. diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index 81310b6c62..f4ecdcea6f 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -1,3 +1,5 @@ +require 'spree/money_decorator' + module Spree module ReportsHelper def report_order_cycle_options(order_cycles) @@ -16,5 +18,8 @@ module Spree orders.map { |o| o.shipping_method.andand.name }.uniq end + def currency_symbol + Spree::Money.currency_symbol + end end end From 532041c07b33ea0e8000fc152a59ec533919921e Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 8 Jan 2015 12:12:15 +1100 Subject: [PATCH 462/681] Use Fuubar formatter for rspec - displays fails as they happen --- .rspec | 1 + Gemfile | 1 + Gemfile.lock | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/.rspec b/.rspec index 333fedd86c..ac8197be16 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,3 @@ --colour --profile +--format Fuubar diff --git a/Gemfile b/Gemfile index 3475cdfe68..d0c4df20b6 100644 --- a/Gemfile +++ b/Gemfile @@ -78,6 +78,7 @@ gem 'jquery-rails' group :test, :development do # Pretty printed test output gem 'turn', '~> 0.8.3', :require => false + gem 'fuubar' gem 'rspec-rails' gem 'shoulda-matchers' gem 'factory_girl_rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index aa79d5f674..d56a77c284 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -269,6 +269,9 @@ GEM railties (>= 3.1.0) sass (>= 3.2.0) fssm (0.2.10) + fuubar (1.3.3) + rspec (>= 2.14.0, < 3.1.0) + ruby-progressbar (~> 1.4) geocoder (1.1.8) gmaps4rails (1.5.6) guard (2.2.4) @@ -449,6 +452,7 @@ GEM rspec-expectations (~> 2.14.0) rspec-mocks (~> 2.14.0) ruby-hmac (0.4.0) + ruby-progressbar (1.7.1) safe_yaml (0.9.5) sass (3.2.19) sass-rails (3.2.6) @@ -548,6 +552,7 @@ DEPENDENCIES foundation-icons-sass-rails foundation-rails foundation_rails_helper! + fuubar geocoder gmaps4rails guard From 1ad13f035919f8514875438f5e86881c9b5608ba Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Sun, 28 Dec 2014 11:22:39 +0000 Subject: [PATCH 463/681] Cleanup of git to remove unnecessary commits from pull request --- .../user_balance_calculator.rb | 17 +++++++++ .../user_balance_calculator_spec.rb | 38 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 lib/open_food_network/user_balance_calculator.rb create mode 100644 spec/lib/open_food_network/user_balance_calculator_spec.rb diff --git a/lib/open_food_network/user_balance_calculator.rb b/lib/open_food_network/user_balance_calculator.rb new file mode 100644 index 0000000000..d0ff3ef9bc --- /dev/null +++ b/lib/open_food_network/user_balance_calculator.rb @@ -0,0 +1,17 @@ +module OpenFoodNetwork + class UserBalanceCalculator + def initialize(user, distributor) + @user = user + @distributor = distributor + end + + def balance + orders = Spree::Order.where(distributor_id: @distributor, user_id: @user) + order_total = orders.sum &:total + + payments = Spree::Payment.where(order_id: orders) + payment_total = payments.sum { |p| p.amount } + payment_total-order_total + end + end +end diff --git a/spec/lib/open_food_network/user_balance_calculator_spec.rb b/spec/lib/open_food_network/user_balance_calculator_spec.rb new file mode 100644 index 0000000000..d4b0ac26db --- /dev/null +++ b/spec/lib/open_food_network/user_balance_calculator_spec.rb @@ -0,0 +1,38 @@ +require 'open_food_network/user_balance_calculator' +require 'spec_helper' + +module OpenFoodNetwork + describe UserBalanceCalculator do + + let!(:usr) { create(:user) } + let!(:entrprs) { create(:distributor_enterprise) } + + describe "for a user and enterprise" do + + let!(:o1) { create(:order_with_totals_and_distribution, user: usr, distributor: entrprs) } #total=10 + let!(:o2) { create(:order_with_totals_and_distribution, user: usr, distributor: entrprs) } #total=10 + let!(:p1) { create(:payment, order: o1, amount: 15.00) } + let!(:p2) { create(:payment, order: o2, amount: 10.00) } + + it "finds the user balance for this enterprise" do + UserBalanceCalculator.new(usr, entrprs).balance.to_i.should == 5 + end + + it "does not find the balance for other enterprises" do + entrprs2 = create(:distributor_enterprise) + o3 = create(:order_with_totals_and_distribution, + user: usr, distributor: entrprs2) #total=10 + p3 = create(:payment, order: o3, amount: 10.00) + UserBalanceCalculator.new(usr, entrprs2).balance.to_i.should == 0 + end + + it "does not find the balance for other users" do + usr2 = create(:user) + o4 = create(:order_with_totals_and_distribution, + user: usr2, distributor: entrprs) #total=10 + p3 = create(:payment, order: o4, amount: 20.00) + UserBalanceCalculator.new(usr2, entrprs).balance.to_i.should == 10 + end + end + end +end From 80c507cc668867a3d4b338181dcb20b41a9d908a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 10:51:57 +1100 Subject: [PATCH 464/681] Formatting --- lib/open_food_network/user_balance_calculator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/open_food_network/user_balance_calculator.rb b/lib/open_food_network/user_balance_calculator.rb index d0ff3ef9bc..32b7d2f1d4 100644 --- a/lib/open_food_network/user_balance_calculator.rb +++ b/lib/open_food_network/user_balance_calculator.rb @@ -10,8 +10,8 @@ module OpenFoodNetwork order_total = orders.sum &:total payments = Spree::Payment.where(order_id: orders) - payment_total = payments.sum { |p| p.amount } - payment_total-order_total + payment_total = payments.sum &:amount + payment_total - order_total end end end From 261dea37e9952625fac0aaf1224aff893bcd0751 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 10:53:24 +1100 Subject: [PATCH 465/681] Spec formatting --- .../user_balance_calculator_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/lib/open_food_network/user_balance_calculator_spec.rb b/spec/lib/open_food_network/user_balance_calculator_spec.rb index d4b0ac26db..192f0278c9 100644 --- a/spec/lib/open_food_network/user_balance_calculator_spec.rb +++ b/spec/lib/open_food_network/user_balance_calculator_spec.rb @@ -4,10 +4,10 @@ require 'spec_helper' module OpenFoodNetwork describe UserBalanceCalculator do - let!(:usr) { create(:user) } - let!(:entrprs) { create(:distributor_enterprise) } + describe "finding the account balance of a user with a hub" do - describe "for a user and enterprise" do + let!(:usr) { create(:user) } + let!(:entrprs) { create(:distributor_enterprise) } let!(:o1) { create(:order_with_totals_and_distribution, user: usr, distributor: entrprs) } #total=10 let!(:o2) { create(:order_with_totals_and_distribution, user: usr, distributor: entrprs) } #total=10 @@ -21,9 +21,9 @@ module OpenFoodNetwork it "does not find the balance for other enterprises" do entrprs2 = create(:distributor_enterprise) o3 = create(:order_with_totals_and_distribution, - user: usr, distributor: entrprs2) #total=10 + user: usr, distributor: entrprs2) #total=10 p3 = create(:payment, order: o3, amount: 10.00) - UserBalanceCalculator.new(usr, entrprs2).balance.to_i.should == 0 + UserBalanceCalculator.new(usr, entrprs2).balance.to_i.should == 0 end it "does not find the balance for other users" do @@ -31,7 +31,7 @@ module OpenFoodNetwork o4 = create(:order_with_totals_and_distribution, user: usr2, distributor: entrprs) #total=10 p3 = create(:payment, order: o4, amount: 20.00) - UserBalanceCalculator.new(usr2, entrprs).balance.to_i.should == 10 + UserBalanceCalculator.new(usr2, entrprs).balance.to_i.should == 10 end end end From 592ac7856a80c1c2e6ed014822f0713ec92acbc9 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 10:58:35 +1100 Subject: [PATCH 466/681] Variable naming --- .../user_balance_calculator_spec.rb | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/lib/open_food_network/user_balance_calculator_spec.rb b/spec/lib/open_food_network/user_balance_calculator_spec.rb index 192f0278c9..9bb8b0bef2 100644 --- a/spec/lib/open_food_network/user_balance_calculator_spec.rb +++ b/spec/lib/open_food_network/user_balance_calculator_spec.rb @@ -6,32 +6,32 @@ module OpenFoodNetwork describe "finding the account balance of a user with a hub" do - let!(:usr) { create(:user) } - let!(:entrprs) { create(:distributor_enterprise) } + let!(:user1) { create(:user) } + let!(:hub1) { create(:distributor_enterprise) } - let!(:o1) { create(:order_with_totals_and_distribution, user: usr, distributor: entrprs) } #total=10 - let!(:o2) { create(:order_with_totals_and_distribution, user: usr, distributor: entrprs) } #total=10 + let!(:o1) { create(:order_with_totals_and_distribution, user: user1, distributor: hub1) } #total=10 + let!(:o2) { create(:order_with_totals_and_distribution, user: user1, distributor: hub1) } #total=10 let!(:p1) { create(:payment, order: o1, amount: 15.00) } let!(:p2) { create(:payment, order: o2, amount: 10.00) } it "finds the user balance for this enterprise" do - UserBalanceCalculator.new(usr, entrprs).balance.to_i.should == 5 + UserBalanceCalculator.new(user1, hub1).balance.to_i.should == 5 end it "does not find the balance for other enterprises" do - entrprs2 = create(:distributor_enterprise) + hub2 = create(:distributor_enterprise) o3 = create(:order_with_totals_and_distribution, - user: usr, distributor: entrprs2) #total=10 + user: user1, distributor: hub2) #total=10 p3 = create(:payment, order: o3, amount: 10.00) - UserBalanceCalculator.new(usr, entrprs2).balance.to_i.should == 0 + UserBalanceCalculator.new(user1, hub2).balance.to_i.should == 0 end it "does not find the balance for other users" do - usr2 = create(:user) + user2 = create(:user) o4 = create(:order_with_totals_and_distribution, - user: usr2, distributor: entrprs) #total=10 + user: user2, distributor: hub1) #total=10 p3 = create(:payment, order: o4, amount: 20.00) - UserBalanceCalculator.new(usr2, entrprs).balance.to_i.should == 10 + UserBalanceCalculator.new(user2, hub1).balance.to_i.should == 10 end end end From 6f36c0463c0dd80a985366016f1956d6bcc15ec1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 11:02:00 +1100 Subject: [PATCH 467/681] Separate data preparation from test conditions --- .../user_balance_calculator_spec.rb | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/spec/lib/open_food_network/user_balance_calculator_spec.rb b/spec/lib/open_food_network/user_balance_calculator_spec.rb index 9bb8b0bef2..5117a44aed 100644 --- a/spec/lib/open_food_network/user_balance_calculator_spec.rb +++ b/spec/lib/open_food_network/user_balance_calculator_spec.rb @@ -18,20 +18,26 @@ module OpenFoodNetwork UserBalanceCalculator.new(user1, hub1).balance.to_i.should == 5 end - it "does not find the balance for other enterprises" do - hub2 = create(:distributor_enterprise) - o3 = create(:order_with_totals_and_distribution, - user: user1, distributor: hub2) #total=10 - p3 = create(:payment, order: o3, amount: 10.00) - UserBalanceCalculator.new(user1, hub2).balance.to_i.should == 0 + context "with another hub" do + let!(:hub2) { create(:distributor_enterprise) } + let!(:o3) { create(:order_with_totals_and_distribution, + user: user1, distributor: hub2) } #total=10 + let!(:p3) { create(:payment, order: o3, amount: 10.00) } + + it "does not find the balance for other enterprises" do + UserBalanceCalculator.new(user1, hub2).balance.to_i.should == 0 + end end - it "does not find the balance for other users" do - user2 = create(:user) - o4 = create(:order_with_totals_and_distribution, - user: user2, distributor: hub1) #total=10 - p3 = create(:payment, order: o4, amount: 20.00) - UserBalanceCalculator.new(user2, hub1).balance.to_i.should == 10 + context "with another user" do + let!(:user2) { create(:user) } + let!(:o4) { create(:order_with_totals_and_distribution, + user: user2, distributor: hub1) } #total=10 + let!(:p3) { create(:payment, order: o4, amount: 20.00) } + + it "does not find the balance for other users" do + UserBalanceCalculator.new(user2, hub1).balance.to_i.should == 10 + end end end end From bbc887a692b991ff73972a350a97341cbfe5fef7 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 11:04:32 +1100 Subject: [PATCH 468/681] Test the exact value, not the truncated value --- spec/lib/open_food_network/user_balance_calculator_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/open_food_network/user_balance_calculator_spec.rb b/spec/lib/open_food_network/user_balance_calculator_spec.rb index 5117a44aed..28a4c00c48 100644 --- a/spec/lib/open_food_network/user_balance_calculator_spec.rb +++ b/spec/lib/open_food_network/user_balance_calculator_spec.rb @@ -15,7 +15,7 @@ module OpenFoodNetwork let!(:p2) { create(:payment, order: o2, amount: 10.00) } it "finds the user balance for this enterprise" do - UserBalanceCalculator.new(user1, hub1).balance.to_i.should == 5 + UserBalanceCalculator.new(user1, hub1).balance.should == 5 end context "with another hub" do @@ -25,7 +25,7 @@ module OpenFoodNetwork let!(:p3) { create(:payment, order: o3, amount: 10.00) } it "does not find the balance for other enterprises" do - UserBalanceCalculator.new(user1, hub2).balance.to_i.should == 0 + UserBalanceCalculator.new(user1, hub2).balance.should == 0 end end @@ -36,7 +36,7 @@ module OpenFoodNetwork let!(:p3) { create(:payment, order: o4, amount: 20.00) } it "does not find the balance for other users" do - UserBalanceCalculator.new(user2, hub1).balance.to_i.should == 10 + UserBalanceCalculator.new(user2, hub1).balance.should == 10 end end end From 7e55262ce9b944cba992665a891bafde413807dc Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 11:20:41 +1100 Subject: [PATCH 469/681] Extract most everything to private methods - neatly groups concerns together --- .../user_balance_calculator.rb | 27 ++++++++++++++----- .../user_balance_calculator_spec.rb | 2 +- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/open_food_network/user_balance_calculator.rb b/lib/open_food_network/user_balance_calculator.rb index 32b7d2f1d4..73926c1d03 100644 --- a/lib/open_food_network/user_balance_calculator.rb +++ b/lib/open_food_network/user_balance_calculator.rb @@ -4,14 +4,29 @@ module OpenFoodNetwork @user = user @distributor = distributor end - + def balance - orders = Spree::Order.where(distributor_id: @distributor, user_id: @user) - order_total = orders.sum &:total - - payments = Spree::Payment.where(order_id: orders) - payment_total = payments.sum &:amount payment_total - order_total end + + + private + + def order_total + orders.sum &:total + end + + def payment_total + payments.sum &:amount + end + + + def orders + Spree::Order.where(distributor_id: @distributor, user_id: @user) + end + + def payments + Spree::Payment.where(order_id: orders) + end end end diff --git a/spec/lib/open_food_network/user_balance_calculator_spec.rb b/spec/lib/open_food_network/user_balance_calculator_spec.rb index 28a4c00c48..9588439f5e 100644 --- a/spec/lib/open_food_network/user_balance_calculator_spec.rb +++ b/spec/lib/open_food_network/user_balance_calculator_spec.rb @@ -39,6 +39,6 @@ module OpenFoodNetwork UserBalanceCalculator.new(user2, hub1).balance.should == 10 end end - end + end end end From 6a42f62eb2fb771d3d32260048735f795e281298 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 11:49:59 +1100 Subject: [PATCH 470/681] Fix filename on order cycle management report csv, add timestamp to it and also users and enterprises report --- app/controllers/spree/admin/reports_controller_decorator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index aaba20d51d..1c42ccdbf3 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -71,7 +71,7 @@ Spree::Admin::ReportsController.class_eval do @orders = @search.result - render_report(@report.header, @report.table, params[:csv], "customers.csv") + render_report(@report.header, @report.table, params[:csv], "order_cycle_management_#{timestamp}.csv") end def orders_and_distributors @@ -602,7 +602,7 @@ Spree::Admin::ReportsController.class_eval do def users_and_enterprises # @report_types = REPORT_TYPES[:users_and_enterprises] @report = OpenFoodNetwork::UsersAndEnterprisesReport.new params - render_report(@report.header, @report.table, params[:csv], "users_and_enterprises.csv") + render_report(@report.header, @report.table, params[:csv], "users_and_enterprises_#{timestamp}.csv") end def render_report (header, table, create_csv, csv_file_name) From 22cb8b7a712501694a9e6518f8a9fd13a872a10e Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 12:33:26 +1100 Subject: [PATCH 471/681] Two-space indentation, change 'Shipping categories' to 'Shipping category' --- .../spree/admin/products/_shipping_category_form.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/spree/admin/products/_shipping_category_form.html.haml b/app/views/spree/admin/products/_shipping_category_form.html.haml index 8460f2c32b..1e324a25b1 100644 --- a/app/views/spree/admin/products/_shipping_category_form.html.haml +++ b/app/views/spree/admin/products/_shipping_category_form.html.haml @@ -1,4 +1,4 @@ = f.field_container :shipping_categories do - = f.label :shipping_category_id, t(:shipping_categories) - = f.collection_select(:shipping_category_id, Spree::ShippingCategory.all, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth'}) - = f.error_message_on :shipping_category_id + = f.label :shipping_category_id, t(:shipping_category) + = f.collection_select(:shipping_category_id, Spree::ShippingCategory.all, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth'}) + = f.error_message_on :shipping_category_id From 27bc845b0b23cd01bff0264652bbaa62d08e8679 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 12:45:35 +1100 Subject: [PATCH 472/681] Add correct markup for tax category required asterisk --- app/views/spree/admin/products/_tax_category_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/products/_tax_category_form.html.haml b/app/views/spree/admin/products/_tax_category_form.html.haml index 9ab348636c..81a8677528 100644 --- a/app/views/spree/admin/products/_tax_category_form.html.haml +++ b/app/views/spree/admin/products/_tax_category_form.html.haml @@ -1,6 +1,6 @@ = f.field_container :tax_category_id do = f.label :tax_category_id, t(:tax_category) - * + %span.required * %br = f.collection_select(:tax_category_id, Spree::TaxCategory.all, :id, :name, {:include_blank => Spree::TaxCategory.count > 1}, {:class => "select2 fullwidth"}) = f.error_message_on :tax_category_id From e9f32f5329356d159404d0608f37fbed60f60ae0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 12:46:36 +1100 Subject: [PATCH 473/681] Spec that shipping category can be set when creating new products --- spec/features/admin/products_spec.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index 0e27930f6e..0d1c3b9c6b 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -18,6 +18,7 @@ feature %q{ describe "creating a product" do let!(:tax_category) { create(:tax_category, name: 'Test Tax Category') } + let!(:shipping_category) { create(:shipping_category, name: 'Test Shipping Category') } scenario "assigning important attributes", js: true do login_to_admin_section @@ -25,7 +26,6 @@ feature %q{ click_link 'Products' click_link 'New Product' - select 'Test Tax Category', from: 'product_tax_category_id' select 'New supplier', from: 'product_supplier_id' fill_in 'product_name', with: 'A new product !!!' select "Weight (kg)", from: 'product_variant_unit_with_scale' @@ -33,13 +33,14 @@ feature %q{ select taxon.name, from: "product_primary_taxon_id" fill_in 'product_price', with: '19.99' fill_in 'product_on_hand', with: 5 + select 'Test Tax Category', from: 'product_tax_category_id' + select 'Test Shipping Category', from: 'product_shipping_category_id' fill_in 'product_description', with: "A description..." click_button 'Create' flash_message.should == 'Product "A new product !!!" has been successfully created!' product = Spree::Product.find_by_name('A new product !!!') - product.tax_category_id.should == tax_category.id product.supplier.should == @supplier product.variant_unit.should == 'weight' product.variant_unit_scale.should == 1000 @@ -49,6 +50,8 @@ feature %q{ product.primary_taxon_id.should == taxon.id product.price.to_s.should == '19.99' product.on_hand.should == 5 + product.tax_category_id.should == tax_category.id + product.shipping_category.should == shipping_category product.description.should == "A description..." product.group_buy.should be_false product.master.option_values.map(&:name).should == ['5kg'] From d40ffeef52136f24bb054a98e38d0b4c647e931b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 13:10:17 +1100 Subject: [PATCH 474/681] Remove old sidebar --- .../admin/enterprises/_sidebar.html.haml | 6 ----- .../_sidebar_enterprise_fees.html.haml | 20 ---------------- .../_sidebar_payment_methods.html.haml | 24 ------------------- .../_sidebar_shipping_methods.html.haml | 23 ------------------ 4 files changed, 73 deletions(-) delete mode 100644 app/views/admin/enterprises/_sidebar.html.haml delete mode 100644 app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml delete mode 100644 app/views/admin/enterprises/_sidebar_payment_methods.html.haml delete mode 100644 app/views/admin/enterprises/_sidebar_shipping_methods.html.haml diff --git a/app/views/admin/enterprises/_sidebar.html.haml b/app/views/admin/enterprises/_sidebar.html.haml deleted file mode 100644 index a4c149c9e9..0000000000 --- a/app/views/admin/enterprises/_sidebar.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if can? :admin, EnterpriseFee - = render 'sidebar_enterprise_fees', f: f -- if can? :admin, Spree::PaymentMethod - = render 'sidebar_payment_methods', f: f -- if can? :admin, Spree::ShippingMethod - = render 'sidebar_shipping_methods', f: f diff --git a/app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml b/app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml deleted file mode 100644 index 566d87b832..0000000000 --- a/app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- enterprise_fees_color = @enterprise_fees.count > 0 ? "blue" : "red" -.sidebar_item.four.columns.alpha#enterprise_fees{ ng: { show: 'Enterprise.category != "producer_hub"' } } - .four.columns.alpha.header{ class: "#{enterprise_fees_color}" } - %span.four.columns.alpha.centered Enterprise Fees - .four.columns.alpha.list{ class: "#{enterprise_fees_color}" } - - if @enterprise_fees.count > 0 - - @enterprise_fees.each do |enterprise_fee| - %a.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.admin_enterprise_fees_path}" } - %span.three.columns.alpha - = enterprise_fee.name - %span.one.column.omega -   - - else - .four.columns.alpha.list-item.red - %span.three.columns.alpha None Available - %span.one.column.omega - %span.icon-remove-sign - %a.four.columns.alpha.button{ href: "#{main_app.admin_enterprise_fees_path}", class: "#{enterprise_fees_color}" } - CREATE NEW - %span.icon-arrow-right diff --git a/app/views/admin/enterprises/_sidebar_payment_methods.html.haml b/app/views/admin/enterprises/_sidebar_payment_methods.html.haml deleted file mode 100644 index 2d8a174e5c..0000000000 --- a/app/views/admin/enterprises/_sidebar_payment_methods.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -.sidebar_item.four.columns.alpha#payment_methods{ ng: { show: 'Enterprise.sells != "none"' } } - .four.columns.alpha.header{ ng: { class: "paymentMethodsColor()" } } - %span.four.columns.alpha.centered Payment Methods - .four.columns.alpha.list{ ng: { class: "paymentMethodsColor()" } } - - if @payment_methods.count > 0 - -# = hidden_field_tag "enterprise[payment_method_ids][]", [] - - @payment_methods.each do |payment_method| - %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'paymentMethodCtrl', init: "findPaymentMethodByID(#{payment_method.id})" } } - %span.four.columns - %span.three.columns.alpha - %label - = f.check_box :payment_method_ids, { multiple: true, 'ng-model' => 'PaymentMethod.selected' }, payment_method.id, nil - = payment_method.name - %a.one.columns.omega{ href: "#{edit_admin_payment_method_path(payment_method)}" } - %span.icon-arrow-right - - else - .four.columns.alpha.list-item - %span.three.columns.alpha None Available - %span.one.column.omega - %span.icon-remove-sign - %a.four.columns.alpha.button{ href: "#{new_admin_payment_method_path}", ng: { class: "paymentMethodsColor()" } } - CREATE NEW - %span.icon-arrow-right - diff --git a/app/views/admin/enterprises/_sidebar_shipping_methods.html.haml b/app/views/admin/enterprises/_sidebar_shipping_methods.html.haml deleted file mode 100644 index 3e9a0434c0..0000000000 --- a/app/views/admin/enterprises/_sidebar_shipping_methods.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -.sidebar_item.four.columns.alpha#shipping_methods{ ng: { show: 'Enterprise.sells != "none"' } } - .four.columns.alpha.header{ ng: { class: "shippingMethodsColor()" } } - %span.four.columns.alpha.centered Shipping Methods - .four.columns.alpha.list{ ng: { class: "shippingMethodsColor()" } } - - if @shipping_methods.count > 0 - - @shipping_methods.each do |shipping_method| - %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } - %span.four.columns - %span.three.columns.alpha - %label - = f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil - = shipping_method.name - %a.one.columns.omega{ href: "#{edit_admin_shipping_method_path(shipping_method)}" } - %span.one.column.alpha.icon-arrow-right - - else - .four.columns.alpha.list-item - %span.three.columns.alpha None Available - %span.one.column.omega - %span.icon-remove-sign - %a.four.columns.alpha.button{ href: "#{new_admin_shipping_method_path}", ng: { class: "shippingMethodsColor()" } } - CREATE NEW - %span.icon-arrow-right - From e013e1fe00b46c6f5622778ad2c99feed22a50fc Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 9 Jan 2015 15:30:49 +1100 Subject: [PATCH 475/681] Don't show rspec profile by default --- .rspec | 1 - 1 file changed, 1 deletion(-) diff --git a/.rspec b/.rspec index ac8197be16..a5a37321ba 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,2 @@ --colour ---profile --format Fuubar From 20f650b4726f989c390ec10bb0af22ba8cd7eef5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 12 Jan 2015 09:32:02 +1100 Subject: [PATCH 476/681] Add VariantOverride.stock_overriden? --- app/models/variant_override.rb | 4 ++++ spec/models/variant_override_spec.rb | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index 9549d02b2e..1efa3b406d 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -16,6 +16,10 @@ class VariantOverride < ActiveRecord::Base self.for(hub, variant).andand.count_on_hand end + def self.stock_overridden?(hub, variant) + count_on_hand_for(hub, variant).present? + end + private diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb index af6ecb91b7..b0048ce853 100644 --- a/spec/models/variant_override_spec.rb +++ b/spec/models/variant_override_spec.rb @@ -40,4 +40,23 @@ describe VariantOverride do VariantOverride.count_on_hand_for(hub, variant).should be_nil end end + + describe "checking if stock levels have been overriden" do + let(:variant) { create(:variant) } + let(:hub) { create(:distributor_enterprise) } + + it "returns true when stock level has been overridden" do + vo = create(:variant_override, variant: variant, hub: hub, count_on_hand: 12) + VariantOverride.stock_overridden?(hub, variant).should be_true + end + + it "returns false when the override has no stock level" do + vo = create(:variant_override, variant: variant, hub: hub, count_on_hand: nil) + VariantOverride.stock_overridden?(hub, variant).should be_false + end + + it "returns false when there is no override for the hub/variant" do + VariantOverride.stock_overridden?(hub, variant).should be_false + end + end end From a4d46225215cfc00af18571ba353093c7f421022 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 12 Jan 2015 10:39:23 +1100 Subject: [PATCH 477/681] Add VariantOverride.decrement_stock --- app/models/variant_override.rb | 14 +++++++++++ spec/models/variant_override_spec.rb | 36 +++++++++++++++++++--------- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index 1efa3b406d..51bb1468f7 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -20,6 +20,20 @@ class VariantOverride < ActiveRecord::Base count_on_hand_for(hub, variant).present? end + def self.decrement_stock!(hub, variant, quantity) + vo = self.for(hub, variant) + + if vo.nil? + Bugsnag.notify RuntimeError.new "Attempting to decrement stock level for a variant without a VariantOverride." + + elsif vo.count_on_hand.blank? + Bugsnag.notify RuntimeError.new "Attempting to decrement stock level on a VariantOverride without a count_on_hand specified." + + else + vo.decrement! :count_on_hand, quantity + end + end + private diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb index b0048ce853..ee06bd20ec 100644 --- a/spec/models/variant_override_spec.rb +++ b/spec/models/variant_override_spec.rb @@ -1,6 +1,9 @@ require 'spec_helper' describe VariantOverride do + let(:variant) { create(:variant) } + let(:hub) { create(:distributor_enterprise) } + describe "scopes" do let(:hub1) { create(:distributor_enterprise) } let(:hub2) { create(:distributor_enterprise) } @@ -14,9 +17,6 @@ describe VariantOverride do end describe "looking up prices" do - let(:variant) { create(:variant) } - let(:hub) { create(:distributor_enterprise) } - it "returns the numeric price when present" do VariantOverride.create!(variant: variant, hub: hub, price: 12.34) VariantOverride.price_for(hub, variant).should == 12.34 @@ -28,9 +28,6 @@ describe VariantOverride do end describe "looking up count on hand" do - let(:variant) { create(:variant) } - let(:hub) { create(:distributor_enterprise) } - it "returns the numeric stock level when present" do VariantOverride.create!(variant: variant, hub: hub, count_on_hand: 12) VariantOverride.count_on_hand_for(hub, variant).should == 12 @@ -42,16 +39,13 @@ describe VariantOverride do end describe "checking if stock levels have been overriden" do - let(:variant) { create(:variant) } - let(:hub) { create(:distributor_enterprise) } - it "returns true when stock level has been overridden" do - vo = create(:variant_override, variant: variant, hub: hub, count_on_hand: 12) + create(:variant_override, variant: variant, hub: hub, count_on_hand: 12) VariantOverride.stock_overridden?(hub, variant).should be_true end it "returns false when the override has no stock level" do - vo = create(:variant_override, variant: variant, hub: hub, count_on_hand: nil) + create(:variant_override, variant: variant, hub: hub, count_on_hand: nil) VariantOverride.stock_overridden?(hub, variant).should be_false end @@ -59,4 +53,24 @@ describe VariantOverride do VariantOverride.stock_overridden?(hub, variant).should be_false end end + + describe "decrementing stock" do + it "decrements stock" do + vo = create(:variant_override, variant: variant, hub: hub, count_on_hand: 12) + VariantOverride.decrement_stock! hub, variant, 2 + vo.reload.count_on_hand.should == 10 + end + + it "silently logs an error if the variant override doesn't have a stock level" do + vo = create(:variant_override, variant: variant, hub: hub, count_on_hand: nil) + Bugsnag.should_receive(:notify) + VariantOverride.decrement_stock! hub, variant, 2 + vo.reload.count_on_hand.should be_nil + end + + it "silently logs an error if the variant override does not exist" do + Bugsnag.should_receive(:notify) + VariantOverride.decrement_stock! hub, variant, 2 + end + end end From 4583e29ae3fe5d853a05551094a20988b44aabba Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 12 Jan 2015 11:23:53 +1100 Subject: [PATCH 478/681] When order is placed, subtract stock from variant override --- app/models/spree/inventory_unit_decorator.rb | 15 ++++++++++ lib/open_food_network/scope_variant_to_hub.rb | 8 ++++++ .../shopping/variant_overrides_spec.rb | 28 +++++++++++++------ 3 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 app/models/spree/inventory_unit_decorator.rb diff --git a/app/models/spree/inventory_unit_decorator.rb b/app/models/spree/inventory_unit_decorator.rb new file mode 100644 index 0000000000..939a97996f --- /dev/null +++ b/app/models/spree/inventory_unit_decorator.rb @@ -0,0 +1,15 @@ +module Spree + InventoryUnit.class_eval do + def self.assign_opening_inventory(order) + return [] unless order.completed? + + #increase inventory to meet initial requirements + order.line_items.each do |line_item| + # Scope variant to hub so that stock levels may be subtracted from VariantOverride. + line_item.variant.scope_to_hub order.distributor + + increase(order, line_item.variant, line_item.quantity) + end + end + end +end diff --git a/lib/open_food_network/scope_variant_to_hub.rb b/lib/open_food_network/scope_variant_to_hub.rb index bb32a3cbf8..e059bdd5bf 100644 --- a/lib/open_food_network/scope_variant_to_hub.rb +++ b/lib/open_food_network/scope_variant_to_hub.rb @@ -11,6 +11,14 @@ module OpenFoodNetwork def count_on_hand VariantOverride.count_on_hand_for(@hub, self) || super end + + def decrement!(attribute, by=1) + if attribute == :count_on_hand && VariantOverride.stock_overridden?(@hub, self) + VariantOverride.decrement_stock! @hub, self, by + else + super + end + end end end diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index 4b73d0b4df..1a0ddfed60 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -7,7 +7,7 @@ feature "shopping with variant overrides defined", js: true do include CheckoutWorkflow include UIComponentHelper - use_short_wait 10 + #use_short_wait 10 describe "viewing products" do let(:hub) { create(:distributor_enterprise, with_payment_and_shipping: true) } @@ -21,16 +21,16 @@ feature "shopping with variant overrides defined", js: true do let(:v1) { create(:variant, product: p1, price: 11.11, unit_value: 1) } let(:v2) { create(:variant, product: p1, price: 22.22, unit_value: 2) } let(:v3) { create(:variant, product: p2, price: 33.33, unit_value: 3) } + let(:v4) { create(:variant, product: p1, price: 44.44, unit_value: 4) } let!(:vo1) { create(:variant_override, hub: hub, variant: v1, price: 55.55) } let!(:vo2) { create(:variant_override, hub: hub, variant: v2, count_on_hand: 0) } let!(:vo3) { create(:variant_override, hub: hub, variant: v3, count_on_hand: 0) } + let!(:vo4) { create(:variant_override, hub: hub, variant: v4, count_on_hand: 3) } let(:ef) { create(:enterprise_fee, enterprise: hub, fee_type: 'packing', calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } before do ActionMailer::Base.deliveries.clear - outgoing_exchange.variants << v1 - outgoing_exchange.variants << v2 - outgoing_exchange.variants << v3 + outgoing_exchange.variants = [v1, v2, v3, v4] outgoing_exchange.enterprise_fees << ef visit shop_path click_link hub.name @@ -111,7 +111,20 @@ feature "shopping with variant overrides defined", js: true do email.body.should include "$122.21" end - it "subtracts stock from the override" + it "subtracts stock from the override" do + fill_in "variants[#{v4.id}]", with: "2" + show_cart + wait_until_enabled 'li.cart a.button' + click_link 'Quick checkout' + + expect do + expect do + complete_checkout + end.to change { v4.reload.count_on_hand }.by(0) + end.to change { vo4.reload.count_on_hand }.by(-2) + end + + it "does not subtract stock from overrides that do not override count_on_hand" end @@ -147,9 +160,6 @@ feature "shopping with variant overrides defined", js: true do end place_order - #sleep 5 - using_wait_time 10 do - page.should have_content "Your order has been processed successfully" - end + page.should have_content "Your order has been processed successfully" end end From d52b6b34e36710eed53245aec3c78e7f74c0db07 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 12 Jan 2015 11:33:53 +1100 Subject: [PATCH 479/681] Spec: do not subtract stock from overrides that do not override count_on_hand --- .../consumer/shopping/variant_overrides_spec.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index 1a0ddfed60..2a49c9420a 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -22,7 +22,7 @@ feature "shopping with variant overrides defined", js: true do let(:v2) { create(:variant, product: p1, price: 22.22, unit_value: 2) } let(:v3) { create(:variant, product: p2, price: 33.33, unit_value: 3) } let(:v4) { create(:variant, product: p1, price: 44.44, unit_value: 4) } - let!(:vo1) { create(:variant_override, hub: hub, variant: v1, price: 55.55) } + let!(:vo1) { create(:variant_override, hub: hub, variant: v1, price: 55.55, count_on_hand: nil) } let!(:vo2) { create(:variant_override, hub: hub, variant: v2, count_on_hand: 0) } let!(:vo3) { create(:variant_override, hub: hub, variant: v3, count_on_hand: 0) } let!(:vo4) { create(:variant_override, hub: hub, variant: v4, count_on_hand: 3) } @@ -124,7 +124,17 @@ feature "shopping with variant overrides defined", js: true do end.to change { vo4.reload.count_on_hand }.by(-2) end - it "does not subtract stock from overrides that do not override count_on_hand" + it "does not subtract stock from overrides that do not override count_on_hand" do + fill_in "variants[#{v1.id}]", with: "2" + show_cart + wait_until_enabled 'li.cart a.button' + click_link 'Quick checkout' + + expect do + complete_checkout + end.to change { v1.reload.count_on_hand }.by(-2) + vo1.reload.count_on_hand.should be_nil + end end From 494bb1f3b44276ba27a19a795d0e040ca676e428 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 12 Jan 2015 14:01:43 +1100 Subject: [PATCH 480/681] Fix auto-reload nerfing scope_xx_to_hub --- app/models/spree/product_decorator.rb | 4 ++++ app/models/spree/variant_decorator.rb | 3 +++ lib/open_food_network/scope_product_to_hub.rb | 10 +++++----- lib/open_food_network/scope_variant_to_hub.rb | 10 +++++----- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index cc8b213bde..0199261cdb 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -1,4 +1,8 @@ +require 'open_food_network/scope_product_to_hub' + Spree::Product.class_eval do + include OpenFoodNetwork::ProductScopableToHub + # We have an after_destroy callback on Spree::ProductOptionType. However, if we # don't specify dependent => destroy on this association, it is not called. See: # https://github.com/rails/rails/issues/7618 diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 5cd5c39155..13908382f9 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -1,7 +1,10 @@ +require 'open_food_network/scope_variant_to_hub' require 'open_food_network/enterprise_fee_calculator' require 'open_food_network/option_value_namer' Spree::Variant.class_eval do + include OpenFoodNetwork::VariantScopableToHub + has_many :exchange_variants, dependent: :destroy has_many :exchanges, through: :exchange_variants diff --git a/lib/open_food_network/scope_product_to_hub.rb b/lib/open_food_network/scope_product_to_hub.rb index 7d5e0aa067..b214abc0d8 100644 --- a/lib/open_food_network/scope_product_to_hub.rb +++ b/lib/open_food_network/scope_product_to_hub.rb @@ -6,11 +6,11 @@ module OpenFoodNetwork super.each { |v| v.scope_to_hub @hub } end end -end -Spree::Product.class_eval do - def scope_to_hub(hub) - extend OpenFoodNetwork::ScopeProductToHub - @hub = hub + module ProductScopableToHub + def scope_to_hub(hub) + extend OpenFoodNetwork::ScopeProductToHub + @hub = hub + end end end diff --git a/lib/open_food_network/scope_variant_to_hub.rb b/lib/open_food_network/scope_variant_to_hub.rb index e059bdd5bf..91ed634043 100644 --- a/lib/open_food_network/scope_variant_to_hub.rb +++ b/lib/open_food_network/scope_variant_to_hub.rb @@ -20,11 +20,11 @@ module OpenFoodNetwork end end end -end -Spree::Variant.class_eval do - def scope_to_hub(hub) - extend OpenFoodNetwork::ScopeVariantToHub - @hub = hub + module VariantScopableToHub + def scope_to_hub(hub) + extend OpenFoodNetwork::ScopeVariantToHub + @hub = hub + end end end From 6b4335438615ca97f9bcfaa5d3e2cc2cb0494618 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 12 Jan 2015 14:37:43 +1100 Subject: [PATCH 481/681] Restructure spec --- .../shopping/variant_overrides_spec.rb | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index 2a49c9420a..c32d9a0b05 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -7,35 +7,33 @@ feature "shopping with variant overrides defined", js: true do include CheckoutWorkflow include UIComponentHelper - #use_short_wait 10 + let(:hub) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let(:producer) { create(:supplier_enterprise) } + let(:oc) { create(:simple_order_cycle, suppliers: [producer], coordinator: hub, distributors: [hub]) } + let(:outgoing_exchange) { oc.exchanges.outgoing.first } + let(:sm) { hub.shipping_methods.first } + let(:pm) { hub.payment_methods.first } + let(:p1) { create(:simple_product, supplier: producer) } + let(:p2) { create(:simple_product, supplier: producer) } + let(:v1) { create(:variant, product: p1, price: 11.11, unit_value: 1) } + let(:v2) { create(:variant, product: p1, price: 22.22, unit_value: 2) } + let(:v3) { create(:variant, product: p2, price: 33.33, unit_value: 3) } + let(:v4) { create(:variant, product: p1, price: 44.44, unit_value: 4) } + let!(:vo1) { create(:variant_override, hub: hub, variant: v1, price: 55.55, count_on_hand: nil) } + let!(:vo2) { create(:variant_override, hub: hub, variant: v2, count_on_hand: 0) } + let!(:vo3) { create(:variant_override, hub: hub, variant: v3, count_on_hand: 0) } + let!(:vo4) { create(:variant_override, hub: hub, variant: v4, count_on_hand: 3) } + let(:ef) { create(:enterprise_fee, enterprise: hub, fee_type: 'packing', calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } + + before do + ActionMailer::Base.deliveries.clear + outgoing_exchange.variants = [v1, v2, v3, v4] + outgoing_exchange.enterprise_fees << ef + visit shop_path + click_link hub.name + end describe "viewing products" do - let(:hub) { create(:distributor_enterprise, with_payment_and_shipping: true) } - let(:producer) { create(:supplier_enterprise) } - let(:oc) { create(:simple_order_cycle, suppliers: [producer], coordinator: hub, distributors: [hub]) } - let(:outgoing_exchange) { oc.exchanges.outgoing.first } - let(:sm) { hub.shipping_methods.first } - let(:pm) { hub.payment_methods.first } - let(:p1) { create(:simple_product, supplier: producer) } - let(:p2) { create(:simple_product, supplier: producer) } - let(:v1) { create(:variant, product: p1, price: 11.11, unit_value: 1) } - let(:v2) { create(:variant, product: p1, price: 22.22, unit_value: 2) } - let(:v3) { create(:variant, product: p2, price: 33.33, unit_value: 3) } - let(:v4) { create(:variant, product: p1, price: 44.44, unit_value: 4) } - let!(:vo1) { create(:variant_override, hub: hub, variant: v1, price: 55.55, count_on_hand: nil) } - let!(:vo2) { create(:variant_override, hub: hub, variant: v2, count_on_hand: 0) } - let!(:vo3) { create(:variant_override, hub: hub, variant: v3, count_on_hand: 0) } - let!(:vo4) { create(:variant_override, hub: hub, variant: v4, count_on_hand: 3) } - let(:ef) { create(:enterprise_fee, enterprise: hub, fee_type: 'packing', calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } - - before do - ActionMailer::Base.deliveries.clear - outgoing_exchange.variants = [v1, v2, v3, v4] - outgoing_exchange.enterprise_fees << ef - visit shop_path - click_link hub.name - end - it "shows the overridden price" do page.should_not have_price "$11.11" page.should have_price "$61.11" @@ -91,7 +89,10 @@ feature "shopping with variant overrides defined", js: true do page.should have_selector 'form.edit_order .shipping', text: '$0.00' page.should have_selector 'form.edit_order .total', text: '$122.21' end + end + + describe "creating orders" do it "creates the order with the correct prices" do fill_in "variants[#{v1.id}]", with: "2" show_cart From 20bde803c8720a65469a49bc3884aacbdd3142f1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 12 Jan 2015 14:58:35 +1100 Subject: [PATCH 482/681] Display overridden prices when products are in the cart --- .../core/controller_helpers/order_decorator.rb | 14 ++++++++++++++ .../consumer/shopping/variant_overrides_spec.rb | 10 +++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/spree/core/controller_helpers/order_decorator.rb b/lib/spree/core/controller_helpers/order_decorator.rb index 727ed7cd90..88ae412ec2 100644 --- a/lib/spree/core/controller_helpers/order_decorator.rb +++ b/lib/spree/core/controller_helpers/order_decorator.rb @@ -1,4 +1,18 @@ Spree::Core::ControllerHelpers::Order.class_eval do + def current_order_with_scoped_variants(create_order_if_necessary = false) + order = current_order_without_scoped_variants(create_order_if_necessary) + + if order + order.line_items.each do |li| + li.variant.scope_to_hub order.distributor + end + end + + order + end + alias_method_chain :current_order, :scoped_variants + + # Override definition in spree/auth/app/controllers/spree/base_controller_decorator.rb # Do not attempt to merge incomplete and current orders. Instead, destroy the incomplete orders. def set_current_order diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index c32d9a0b05..f7b1773e33 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -35,7 +35,7 @@ feature "shopping with variant overrides defined", js: true do describe "viewing products" do it "shows the overridden price" do - page.should_not have_price "$11.11" + page.should_not have_price "$12.22" # $11.11 + 10% fee page.should have_price "$61.11" end @@ -56,6 +56,14 @@ feature "shopping with variant overrides defined", js: true do page.should have_selector 'li.total div', text: '= $61.11' end + it "shows the correct prices when products are in the cart" do + fill_in "variants[#{v1.id}]", with: "2" + show_cart + wait_until_enabled 'li.cart a.button' + visit shop_path + page.should_not have_price '$12.22' + end + # The two specs below reveal an unrelated issue with fee calculation. See: # https://github.com/openfoodfoundation/openfoodnetwork/issues/312 From 307cc313df24ec2e33498c12dda7f0dffcf834f1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 13 Jan 2015 11:46:41 +1100 Subject: [PATCH 483/681] Stub scope_to_hub in spec that wasn't expecting it --- spec/models/spree/order_populator_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/models/spree/order_populator_spec.rb b/spec/models/spree/order_populator_spec.rb index bfe2e123f8..5602e46da5 100644 --- a/spec/models/spree/order_populator_spec.rb +++ b/spec/models/spree/order_populator_spec.rb @@ -48,6 +48,7 @@ module Spree describe "attempt_cart_add" do it "performs additional validations" do variant = double(:variant) + variant.stub(:scope_to_hub) quantity = 123 Spree::Variant.stub(:find).and_return(variant) From 0e510998a490915512bbc581d1e720c6f40aaf57 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 14 Jan 2015 14:33:37 +1100 Subject: [PATCH 484/681] Handling missing options in shipping filter call --- .../javascripts/darkswarm/filters/shipping.js.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/darkswarm/filters/shipping.js.coffee b/app/assets/javascripts/darkswarm/filters/shipping.js.coffee index 761e34337d..7879568f0d 100644 --- a/app/assets/javascripts/darkswarm/filters/shipping.js.coffee +++ b/app/assets/javascripts/darkswarm/filters/shipping.js.coffee @@ -1,9 +1,10 @@ -Darkswarm.filter 'shipping', ()-> +Darkswarm.filter 'shipping', ()-> (objects, options)-> objects ||= [] - options ?= null - - if options.pickup and !options.delivery + + if !options + objects + else if options.pickup and !options.delivery objects.filter (obj)-> obj.pickup else if options.delivery and !options.pickup From 9f1a773a58b28772d576c88f2bd6de961f60ee16 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 14 Jan 2015 14:34:31 +1100 Subject: [PATCH 485/681] MapController: copy default config instead of referencing Copying makes it possible to have multiple independent maps. --- .../javascripts/darkswarm/controllers/map_controller.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/controllers/map_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/map_controller.js.coffee index 7b0cd3607b..853e86d510 100644 --- a/app/assets/javascripts/darkswarm/controllers/map_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/map_controller.js.coffee @@ -1,3 +1,3 @@ Darkswarm.controller "MapCtrl", ($scope, MapConfiguration, OfnMap)-> $scope.OfnMap = OfnMap - $scope.map = MapConfiguration.options + $scope.map = angular.copy MapConfiguration.options From 02a276b9c91eecb64177fbfbc2209c0783920619 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 14 Jan 2015 15:18:46 +1100 Subject: [PATCH 486/681] Producer and hub lists with search Fixing map display as well. --- ...group_enterprise_node_controller.js.coffee | 12 ++++ .../group_enterprises_controller.js.coffee | 13 ++++ app/views/groups/show.html.haml | 63 +++++++++++++++---- 3 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee create mode 100644 app/assets/javascripts/darkswarm/controllers/group_enterprises_controller.js.coffee diff --git a/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee new file mode 100644 index 0000000000..31c2354a53 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee @@ -0,0 +1,12 @@ +Darkswarm.controller "GroupEnterpriseNodeCtrl", ($scope, CurrentHub) -> + + $scope.active = false + + $scope.toggle = () -> + $scope.active = !$scope.active + + $scope.open = -> + $scope.active + + $scope.current = -> + $scope.hub.id is CurrentHub.hub.id diff --git a/app/assets/javascripts/darkswarm/controllers/group_enterprises_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/group_enterprises_controller.js.coffee new file mode 100644 index 0000000000..ab8336116f --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/group_enterprises_controller.js.coffee @@ -0,0 +1,13 @@ +Darkswarm.controller "GroupEnterprisesCtrl", ($scope, Enterprises, Search, FilterSelectorsService) -> + $scope.Enterprises = Enterprises + $scope.totalActive = FilterSelectorsService.totalActive + $scope.clearAll = FilterSelectorsService.clearAll + $scope.filterText = FilterSelectorsService.filterText + $scope.FilterSelectorsService = FilterSelectorsService + $scope.query = Search.search() + $scope.activeTaxons = [] + $scope.show_profiles = false + $scope.filtersActive = false + + $scope.$watch "query", (query)-> + Search.search query diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 0d5fe29359..2ec509c0c5 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -8,35 +8,74 @@ #div{"ng-controller" => "TabsCtrl", style: "clear: both"} %tabset + %tab{heading: 'Map', active: "active(\'\')", select: "select(\'\')"} - %p TODO: show a map of enterprises = inject_json_ams "enterprises", @group.enterprises, Api::EnterpriseSerializer .map-container - %map{"ng-controller" => "MapCtrl"} + %map{"ng-controller" => "MapCtrl", "ng-if" => "(active(\'\') && (mapShowed = true)) || mapShowed"} %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} %markers{models: "OfnMap.enterprises", fit: "true", coords: "'self'", icon: "'icon'", click: "'reveal'"} + %tab{heading: 'About us', - active: "active(\'hi\')", - select: "select(\'hi\')"} + active: "active(\'about\')", + select: "select(\'about\')"} %h3 About us %p= @group.long_description + %tab{heading: 'Our producers', active: "active(\'producers\')", select: "select(\'producers\')"} - %h3 Our producers - %ul - - @group.enterprises.is_primary_producer.each do |enterprise| - %li= link_to enterprise.name, enterprise + .producers.pad-top{"ng-controller" => "GroupEnterprisesCtrl"} + .row + .small-12.columns.pad-top + %h1 Our Producers + = render partial: "shared/components/enterprise_search" + -# TODO: find out why this is not working + -#= render partial: "producers/filters" + + .row{bindonce: true} + .small-12.columns + .active_table + %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", + "ng-repeat" => "producer in filteredEnterprises = (Enterprises.producers | visible | searchEnterprises:query | taxons:activeTaxons)", + "ng-controller" => "GroupEnterpriseNodeCtrl", + "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !producer.active}", + id: "{{producer.hash}}"} + + .small-12.columns + = render partial: 'producers/skinny' + = render partial: 'producers/fat' + + = render partial: 'shared/components/enterprise_no_results' + %tab{heading: 'Our hubs', active: "active(\'hubs\')", select: "select(\'hubs\')"} - %h3 Our producers - %ul - - @group.enterprises.is_distributor.each do |enterprise| - %li= link_to enterprise.name, enterprise + #hubs.hubs{"ng-controller" => "GroupEnterprisesCtrl"} + .row + .small-12.columns + %h1 Our Hubs + + = render partial: "shared/components/enterprise_search" + -# TODO: find out why this is not working + -#= render partial: "home/filters" + + .row{bindonce: true} + .small-12.columns + .active_table + %hub.active_table_node.row.animate-repeat{id: "{{hub.hash}}", + "ng-repeat" => "hub in filteredEnterprises = (Enterprises.hubs | visible | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])", + "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", + "ng-controller" => "GroupEnterpriseNodeCtrl"} + .small-12.columns + = render partial: 'home/skinny' + = render partial: 'home/fat' + + = render partial: 'shared/components/enterprise_no_results' + %tab{heading: 'Contact us', active: "active(\'contact\')", select: "select(\'contact\')"} From 949808e839f25bc38fefd0bc8b4f125c520b28da Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 14 Jan 2015 18:03:08 +1100 Subject: [PATCH 487/681] Customising AdaptiveMenu for the admin tab panel The AdaptiveMenu was design for a menu filling the screen width. Our menu is in a skeleton structure. The new file overwrites the spree version and takes the container width as reference. --- .../assets/javascripts/jquery.adaptivemenu.js | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 vendor/assets/javascripts/jquery.adaptivemenu.js diff --git a/vendor/assets/javascripts/jquery.adaptivemenu.js b/vendor/assets/javascripts/jquery.adaptivemenu.js new file mode 100644 index 0000000000..8df8b98c65 --- /dev/null +++ b/vendor/assets/javascripts/jquery.adaptivemenu.js @@ -0,0 +1,62 @@ +/* + * Original from spree/core/vendor/assets/javascripts/jquery.adaptivemenu.js + */ + +/* + * Used for the spree admin tab bar (Orders, Products, Reports etc.). + * Using parent's width instead of window width. + */ +jQuery.fn.AdaptiveMenu = function(options){ + + var options = jQuery.extend({ + text: "More...", + accuracy:0, + 'class':null, + 'classLinckMore':null + },options); + + var menu = this; + var li = $(menu).find("li"); + + var width = 0; + var widthLi = []; + $.each( li , function(i, l){ + width += $(l).width(); + widthLi.push( width ); + }); + + var buildingMenu = function(){ + var windowWidth = $(menu.parent()).width() - options.accuracy; + for(var i = 0; i windowWidth ) + $( li[i] ).hide(); + else + $( li[i] ).show(); + } + $(menu).find('#more').remove(); + var hideLi = $(li).filter(':not(:visible)'); + var lastLi = $(li).filter(':visible').last(); + if ( hideLi.length > 0 ){ + var more = $("

  • ") + .css({"display":"inline-block","white-space":"nowrap"}) + .addClass(options.classLinckMore) + .attr({"id":"more"}) + .html(options.text) + .click(function(){$(this).find('li').toggle()}); + + var ul = $("
      ") + .css({"position":"absolute"}) + .addClass(options.klass) + .html(hideLi.clone()).prepend(lastLi.clone().hide()); + + more.append(ul); + + lastLi.hide().before(more); + } + } + + jQuery(window).resize(buildingMenu); + + jQuery(window).ready(buildingMenu); + +}; From 13cbbcef404e84fe18c15063d17bbc80b820796f Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 15 Jan 2015 10:26:27 +1100 Subject: [PATCH 488/681] Admin Tab Menu: converting .rb to .html.haml.deface Plus a new menu item for enterprise groups. --- app/overrides/add_enterprises_admin_tab.rb | 5 ----- app/overrides/add_order_cycles_admin_tab.rb | 5 ----- .../layouts/admin/add_enterprises_admin_tab.html.haml.deface | 2 ++ .../layouts/admin/add_groups_admin_tab.html.haml.deface | 2 ++ .../admin/add_order_cycles_admin_tab.html.haml.deface | 2 ++ 5 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 app/overrides/add_enterprises_admin_tab.rb delete mode 100644 app/overrides/add_order_cycles_admin_tab.rb create mode 100644 app/overrides/spree/layouts/admin/add_enterprises_admin_tab.html.haml.deface create mode 100644 app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface create mode 100644 app/overrides/spree/layouts/admin/add_order_cycles_admin_tab.html.haml.deface diff --git a/app/overrides/add_enterprises_admin_tab.rb b/app/overrides/add_enterprises_admin_tab.rb deleted file mode 100644 index 9fbb00f5f3..0000000000 --- a/app/overrides/add_enterprises_admin_tab.rb +++ /dev/null @@ -1,5 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/layouts/admin", - :name => "add_enterprises_admin_tab", - :insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]", - :text => "<%= tab :enterprises, :url => main_app.admin_enterprises_path %>", - :original => '6999548b86c700f2cc5d4f9d297c94b3617fd981') \ No newline at end of file diff --git a/app/overrides/add_order_cycles_admin_tab.rb b/app/overrides/add_order_cycles_admin_tab.rb deleted file mode 100644 index 6e36e413a8..0000000000 --- a/app/overrides/add_order_cycles_admin_tab.rb +++ /dev/null @@ -1,5 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/layouts/admin", - :name => "add_order_cycles_admin_tab", - :insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]", - :text => "<%= tab :order_cycles, :url => main_app.admin_order_cycles_path %>", - :original => 'd4e321201ecb543e92192a031c8896a45dde3576') \ No newline at end of file diff --git a/app/overrides/spree/layouts/admin/add_enterprises_admin_tab.html.haml.deface b/app/overrides/spree/layouts/admin/add_enterprises_admin_tab.html.haml.deface new file mode 100644 index 0000000000..cddadabed7 --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_enterprises_admin_tab.html.haml.deface @@ -0,0 +1,2 @@ +/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]" += tab :enterprises, :url => main_app.admin_enterprises_path diff --git a/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface b/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface new file mode 100644 index 0000000000..8ed9cf55fd --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface @@ -0,0 +1,2 @@ +/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]" += tab :groups, :url => main_app.admin_enterprise_groups_path diff --git a/app/overrides/spree/layouts/admin/add_order_cycles_admin_tab.html.haml.deface b/app/overrides/spree/layouts/admin/add_order_cycles_admin_tab.html.haml.deface new file mode 100644 index 0000000000..90b9007de5 --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_order_cycles_admin_tab.html.haml.deface @@ -0,0 +1,2 @@ +/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]" += tab :order_cycles, :url => main_app.admin_order_cycles_path From 3e5dfda324d82c1a8a769af62f1165734c5bdd41 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 15 Jan 2015 10:53:11 +1100 Subject: [PATCH 489/681] Removing old link to enterprise groups from configuration menu --- .../add_enterprise_groups_to_admin_configurations_menu.rb | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 app/overrides/add_enterprise_groups_to_admin_configurations_menu.rb diff --git a/app/overrides/add_enterprise_groups_to_admin_configurations_menu.rb b/app/overrides/add_enterprise_groups_to_admin_configurations_menu.rb deleted file mode 100644 index 9fb511fc8a..0000000000 --- a/app/overrides/add_enterprise_groups_to_admin_configurations_menu.rb +++ /dev/null @@ -1,6 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/admin/shared/_configuration_menu", - :name => "add_enterprise_groups_to_admin_configurations_menu", - :insert_bottom => "[data-hook='admin_configurations_sidebar_menu']", - :text => "
    • <%= link_to 'Enterprise Groups', main_app.admin_enterprise_groups_path %>
    • ", - :partial => 'enterprise_groups/admin_configurations_menu', - :original => '') From 06cf9141191ed8a06710a6480edfdf2e1434a646 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 15 Jan 2015 12:10:05 +1100 Subject: [PATCH 490/681] Hopefully resolving order populator strangeness related to concurrency by ensuring a fresh list of line items --- app/models/spree/order_decorator.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 919c79fe2b..c4dfeaeb64 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -101,6 +101,7 @@ Spree::Order.class_eval do # Overridden to support max_quantity def add_variant(variant, quantity = 1, max_quantity = nil, currency = nil) + line_items(:reload) current_item = find_line_item_by_variant(variant) if current_item Bugsnag.notify(RuntimeError.new("Order populator weirdness"), { From 2170c7ede1d1f0e4d5c3bdb96be1e7d585860288 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 14 Jan 2015 09:36:41 +1100 Subject: [PATCH 491/681] Fix broken JS spec --- app/assets/javascripts/admin/taxons/services/taxons.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/taxons/services/taxons.js.coffee b/app/assets/javascripts/admin/taxons/services/taxons.js.coffee index 4f7faa7c9c..62daf77901 100644 --- a/app/assets/javascripts/admin/taxons/services/taxons.js.coffee +++ b/app/assets/javascripts/admin/taxons/services/taxons.js.coffee @@ -13,7 +13,7 @@ angular.module("admin.taxons").factory "Taxons", (taxons, $filter) -> # For finding multiple Taxons represented by comma delimited string findByIDs: (ids) -> - @taxonsByID[taxon_id] for taxon_id in ids.split(",") + @taxonsByID[taxon_id] for taxon_id in ids.split(",") when @taxonsByID[taxon_id] findByTerm: (term) -> $filter('filter')(@taxons, term) From a93a824b83e2321d31b2335eed7d16d41d9e9e78 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 14 Jan 2015 10:02:23 +1100 Subject: [PATCH 492/681] Admin can grant permission for an enterprise to edit the profile of another --- .../admin/services/enterprise_relationships.js.coffee | 2 ++ spec/features/admin/enterprise_relationships_spec.rb | 5 +++-- .../admin/services/enterprise_relationships_spec.js.coffee | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index 16c9c38fea..cb6542e3eb 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -4,6 +4,7 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris all_permissions: [ 'add_to_order_cycle' 'manage_products' + 'edit_profile' ] constructor: -> @@ -26,3 +27,4 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris switch permission when "add_to_order_cycle" then "to add to order cycle" when "manage_products" then "to manage products" + when "edit_profile" then "to edit profile" diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 5e4366b6f7..83576c7456 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -42,13 +42,14 @@ feature %q{ check 'to add to order cycle' check 'to manage products' uncheck 'to manage products' + check 'to edit profile' select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2, ['to add to order cycle'] + page.should have_relationship e1, e2, ['to add to order cycle', 'to edit profile'] er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first er.should be_present - er.permissions.map(&:name).should == ['add_to_order_cycle'] + er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'edit_profile'].sort end diff --git a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee index d179f6857c..c7ebb1b1ba 100644 --- a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -14,3 +14,4 @@ describe "enterprise relationships", -> it "presents permission names", -> expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "to add to order cycle" expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "to manage products" + expect(EnterpriseRelationships.permission_presentation("edit_profile")).toEqual "to edit profile" From cfb69ae7d27cc25a21495fab9395d40fe97756a8 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 14 Jan 2015 11:02:38 +1100 Subject: [PATCH 493/681] Add Permissions#editable_enterprises --- lib/open_food_network/permissions.rb | 5 +++++ spec/lib/open_food_network/permissions_spec.rb | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index ca87f92b4e..f6d6757d67 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -15,6 +15,11 @@ module OpenFoodNetwork managed_and_related_enterprises_with :add_to_order_cycle end + # Find enterprises for which an admin is allowed to edit their profile + def editable_enterprises + managed_and_related_enterprises_with :edit_profile + end + # For every hub that an admin manages, show all the producers that that hub may add # to the order cycle # {hub1_id => [producer1_id, producer2_id, ...], ...} diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index 6ce369fbb5..184494153b 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -21,6 +21,19 @@ module OpenFoodNetwork end end + describe "finding enterprises whose profiles can be edited" do + let(:e) { double(:enterprise) } + + it "returns managed and related enterprises with edit_profile permission" do + permissions. + should_receive(:managed_and_related_enterprises_with). + with(:edit_profile). + and_return([e]) + + permissions.editable_enterprises.should == [e] + end + end + describe "finding enterprises that can be added to an order cycle, for each hub" do let!(:hub) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } From 90ad2e2b7d9f2a6067c28789ae882710e70450c7 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 14 Jan 2015 11:03:16 +1100 Subject: [PATCH 494/681] Allow enterprises with 'edit profile' permission to edit enterprises --- app/models/spree/ability_decorator.rb | 2 +- spec/models/spree/ability_spec.rb | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 23138fcea9..ca870f05b8 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -52,7 +52,7 @@ class AbilityDecorator can [:admin, :index, :create], Enterprise can [:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], Enterprise do |enterprise| - user.enterprises.include? enterprise + OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise end # All enterprises can have fees, though possibly suppliers don't need them? diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index dee6f007de..92d52e8d73 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -123,7 +123,7 @@ module Spree let(:er1) { create(:enterprise_relationship, parent: s1, child: d1) } let(:er2) { create(:enterprise_relationship, parent: d1, child: s1) } - let(:er_p) { create(:enterprise_relationship, parent: s_related, child: s1, permissions_list: [:manage_products]) } + let(:er_ps) { create(:enterprise_relationship, parent: s_related, child: s1, permissions_list: [:manage_products]) } subject { user } let(:user) { nil } @@ -242,6 +242,19 @@ module Spree let(:vo1) { create(:variant_override, hub: d1, variant: p1.master) } let(:vo2) { create(:variant_override, hub: d2, variant: p2.master) } + describe "editing enterprises" do + let!(:d_related) { create(:distributor_enterprise) } + let!(:er_pd) { create(:enterprise_relationship, parent: d_related, child: d1, permissions_list: [:edit_profile]) } + + it "should be able to edit enterprises it manages" do + should have_ability([:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], for: d1) + end + + it "should be able to edit enterprises it has permission to" do + should have_ability([:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], for: d_related) + end + end + describe "variant overrides" do it "should be able to access variant overrides page" do should have_ability([:admin, :index, :bulk_update], for: VariantOverride) From f6e9c9494c0213bb627f84718ab2ed06306b7da5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 14 Jan 2015 11:03:32 +1100 Subject: [PATCH 495/681] Enterprise user can edit profiles it has permission to --- .../admin/enterprises_controller.rb | 6 ++++-- spec/features/admin/enterprises_spec.rb | 19 ++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 0ee0541145..cf97b5a283 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -76,8 +76,10 @@ module Admin end def collection - # TODO was ordered with is_distributor DESC as well, not sure why or how we want ot sort this now - Enterprise.managed_by(spree_current_user).order('is_primary_producer ASC, name') + # TODO was ordered with is_distributor DESC as well, not sure why or how we want to sort this now + OpenFoodNetwork::Permissions.new(spree_current_user). + editable_enterprises. + order('is_primary_producer ASC, name') end def collection_actions diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index e7f7ea14cf..e19249fef1 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -324,7 +324,9 @@ feature %q{ let(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } let(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } let(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } + let(:distributor3) { create(:distributor_enterprise, name: 'Yet Another Distributor') } let(:enterprise_user) { create_enterprise_user } + let!(:er) { create(:enterprise_relationship, parent: distributor3, child: distributor1, permissions_list: [:edit_profile]) } before(:each) do enterprise_user.enterprise_roles.build(enterprise: supplier1).save @@ -410,15 +412,26 @@ feature %q{ end end - scenario "editing enterprises I have permission to" do + scenario "editing enterprises I manage" do click_link 'Enterprises' - within('#listing_enterprises tbody tr:first') { click_link 'Edit Profile' } + within("#listing_enterprises tr.enterprise-#{distributor1.id}") { click_link 'Edit Profile' } fill_in 'enterprise_name', :with => 'Eaterprises' click_button 'Update' flash_message.should == 'Enterprise "Eaterprises" has been successfully updated!' - page.should have_field 'enterprise_name', :with => 'Eaterprises' + distributor1.reload.name.should == 'Eaterprises' + end + + scenario "editing enterprises I have permission to" do + click_link 'Enterprises' + within("#listing_enterprises tr.enterprise-#{distributor3.id}") { click_link 'Edit Profile' } + + fill_in 'enterprise_name', :with => 'Eaterprises' + click_button 'Update' + + flash_message.should == 'Enterprise "Eaterprises" has been successfully updated!' + distributor3.reload.name.should == 'Eaterprises' end scenario "editing images for an enterprise" do From 642de2f65f7b1ff50386d61353020aa3a24911e0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 14 Jan 2015 12:16:49 +1100 Subject: [PATCH 496/681] Only show delete enterprise link when user has permission --- app/views/admin/enterprises/_actions.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/admin/enterprises/_actions.html.haml b/app/views/admin/enterprises/_actions.html.haml index 69dd6569a8..c41cf290bc 100644 --- a/app/views/admin/enterprises/_actions.html.haml +++ b/app/views/admin/enterprises/_actions.html.haml @@ -1,8 +1,9 @@ = link_to_with_icon('icon-edit', 'Edit Profile', main_app.edit_admin_enterprise_path(enterprise), class: 'edit') %br/ -= link_to_delete_enterprise enterprise -%br/ +- if can? :destroy, enterprise + = link_to_delete_enterprise enterprise + %br/ - if enterprise.is_primary_producer = link_to_with_icon 'icon-dashboard', 'Properties', main_app.admin_enterprise_producer_properties_path(enterprise_id: enterprise.id) From f0bd9c1065f26c24ed34077ef0699b0c97dd2f46 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 14 Jan 2015 14:42:31 +1100 Subject: [PATCH 497/681] Fix failing specs due to addition of another enterprise --- spec/features/admin/enterprises_spec.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index e19249fef1..112052452d 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -326,11 +326,12 @@ feature %q{ let(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } let(:distributor3) { create(:distributor_enterprise, name: 'Yet Another Distributor') } let(:enterprise_user) { create_enterprise_user } - let!(:er) { create(:enterprise_relationship, parent: distributor3, child: distributor1, permissions_list: [:edit_profile]) } + let(:er) { create(:enterprise_relationship, parent: distributor3, child: distributor1, permissions_list: [:edit_profile]) } before(:each) do enterprise_user.enterprise_roles.build(enterprise: supplier1).save enterprise_user.enterprise_roles.build(enterprise: distributor1).save + er login_to_admin_as enterprise_user end @@ -348,10 +349,16 @@ feature %q{ expect(page).to_not have_select "enterprise_set_collection_attributes_0_sells" end + within("tr.enterprise-#{distributor3.id}") do + expect(page).to have_content distributor3.name + expect(page).to have_unchecked_field "enterprise_set_collection_attributes_1_is_primary_producer" + expect(page).to_not have_select "enterprise_set_collection_attributes_1_sells" + end + within("tr.enterprise-#{supplier1.id}") do expect(page).to have_content supplier1.name - expect(page).to have_checked_field "enterprise_set_collection_attributes_1_is_primary_producer" - expect(page).to_not have_select "enterprise_set_collection_attributes_1_sells" + expect(page).to have_checked_field "enterprise_set_collection_attributes_2_is_primary_producer" + expect(page).to_not have_select "enterprise_set_collection_attributes_2_sells" end expect(page).to_not have_content "supplier2.name" From d5437e15082b31e10bbf4e51c16cad81a2489846 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 14 Jan 2015 14:46:59 +1100 Subject: [PATCH 498/681] edit_profile permission no longer shows links to manage shipping methods, payment methods and enterprise fees for an enterprise --- app/models/spree/ability_decorator.rb | 3 +++ .../admin/enterprises/_actions.html.haml | 6 ++--- spec/features/admin/enterprises_spec.rb | 25 +++++++++++++------ spec/models/spree/ability_spec.rb | 8 ++++++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index ca870f05b8..6c59887730 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -54,6 +54,9 @@ class AbilityDecorator can [:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], Enterprise do |enterprise| OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise end + can [:manage_payment_methods, :manage_shipping_methods, :manage_enterprise_fees], Enterprise do |enterprise| + user.enterprises.include? enterprise + end # All enterprises can have fees, though possibly suppliers don't need them? can [:index, :create], EnterpriseFee diff --git a/app/views/admin/enterprises/_actions.html.haml b/app/views/admin/enterprises/_actions.html.haml index c41cf290bc..2ca78f75df 100644 --- a/app/views/admin/enterprises/_actions.html.haml +++ b/app/views/admin/enterprises/_actions.html.haml @@ -11,21 +11,21 @@ %br/ - if enterprise.is_distributor - - if can? :admin, Spree::PaymentMethod + - if can?(:admin, Spree::PaymentMethod) && can?(:manage_payment_methods, enterprise) = link_to_with_icon 'icon-chevron-right', 'Payment Methods', spree.admin_payment_methods_path(enterprise_id: enterprise.id) (#{enterprise.payment_methods.count}) - if enterprise.payment_methods.count == 0 %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} %br/ - - if can? :admin, Spree::ShippingMethod + - if can?(:admin, Spree::ShippingMethod) && can?(:manage_shipping_methods, enterprise) = link_to_with_icon 'icon-plane', 'Shipping Methods', spree.admin_shipping_methods_path(enterprise_id: enterprise.id) (#{enterprise.shipping_methods.count}) - if enterprise.shipping_methods.count == 0 %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} %br/ -- if can? :admin, EnterpriseFee +- if can?(:admin, EnterpriseFee) && can?(:manage_enterprise_fees, enterprise) = link_to_with_icon 'icon-money', 'Enterprise Fees', main_app.admin_enterprise_fees_path(enterprise_id: enterprise.id) (#{enterprise.enterprise_fees.count}) - if enterprise.enterprise_fees.count == 0 diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 112052452d..dfd7afb6aa 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -430,15 +430,26 @@ feature %q{ distributor1.reload.name.should == 'Eaterprises' end - scenario "editing enterprises I have permission to" do - click_link 'Enterprises' - within("#listing_enterprises tr.enterprise-#{distributor3.id}") { click_link 'Edit Profile' } + describe "enterprises I have edit permission for, but do not manage" do + it "allows me to edit them" do + click_link 'Enterprises' + within("#listing_enterprises tr.enterprise-#{distributor3.id}") { click_link 'Edit Profile' } - fill_in 'enterprise_name', :with => 'Eaterprises' - click_button 'Update' + fill_in 'enterprise_name', :with => 'Eaterprises' + click_button 'Update' - flash_message.should == 'Enterprise "Eaterprises" has been successfully updated!' - distributor3.reload.name.should == 'Eaterprises' + flash_message.should == 'Enterprise "Eaterprises" has been successfully updated!' + distributor3.reload.name.should == 'Eaterprises' + end + + it "does not show links to manage payment methods, shipping methods or enterprise fees" do + click_link 'Enterprises' + within("#listing_enterprises tr.enterprise-#{distributor3.id}") do + page.should_not have_link 'Payment Methods' + page.should_not have_link 'Shipping Methods' + page.should_not have_link 'Enterprise Fees' + end + end end scenario "editing images for an enterprise" do diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 92d52e8d73..4a05c56b87 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -253,6 +253,14 @@ module Spree it "should be able to edit enterprises it has permission to" do should have_ability([:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], for: d_related) end + + it "should be able to manage shipping methods, payment methods and enterprise fees for enterprises it manages" do + should have_ability([:manage_shipping_methods, :manage_payment_methods, :manage_enterprise_fees], for: d1) + end + + it "should not be able to manage shipping methods, payment methods and enterprise fees for enterprises it has edit profile permission to" do + should_not have_ability([:manage_shipping_methods, :manage_payment_methods, :manage_enterprise_fees], for: d_related) + end end describe "variant overrides" do From 2d82f76a43c9fce3f9ea87e096616192c132b705 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 14 Jan 2015 14:52:43 +1100 Subject: [PATCH 499/681] Fix changed var in ability spec --- spec/models/spree/ability_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 4a05c56b87..0c1dd9eb32 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -145,7 +145,7 @@ module Spree end it "should be able to read/write related enterprises' products and variants with manage_products permission" do - er_p + er_ps should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p_related) should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p_related.master) end From 83754a01effeb230890ac080a7e1f6cecfc978ff Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 15 Jan 2015 11:34:01 +1100 Subject: [PATCH 500/681] Make side menu item visibility declarative --- .../enterprise_controller.js.coffee | 22 ------------------ .../side_menu_controller.js.coffee | 23 +++++++++++++++---- .../admin/enterprises/_side_menu.html.haml | 3 ++- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index e17e2e9c21..d218f7caea 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -44,25 +44,3 @@ angular.module("admin.enterprises") count++ if shipping_method.selected count , 0 - - $scope.$watch "Enterprise.is_primary_producer", (newValue, oldValue) -> - if !newValue && $scope.Enterprise.sells == "none" - $scope.menu.hide_item_by_name('Enterprise Fees') - else - $scope.menu.show_item_by_name('Enterprise Fees') - - - $scope.$watch "Enterprise.sells", (newValue, oldValue) -> - if newValue == "none" - $scope.menu.hide_item_by_name('Shipping Methods') - $scope.menu.hide_item_by_name('Payment Methods') - $scope.menu.hide_item_by_name('Shop Preferences') - if $scope.Enterprise.is_primary_producer - $scope.menu.show_item_by_name('Enterprise Fees') - else - $scope.menu.hide_item_by_name('Enterprise Fees') - else - $scope.menu.show_item_by_name('Shipping Methods') - $scope.menu.show_item_by_name('Payment Methods') - $scope.menu.show_item_by_name('Shop Preferences') - $scope.menu.show_item_by_name('Enterprise Fees') diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index b893373b98..dddaa1a37d 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -1,5 +1,5 @@ angular.module("admin.enterprises") - .controller "sideMenuCtrl", ($scope, Enterprise, SideMenu) -> + .controller "sideMenuCtrl", ($scope, $parse, Enterprise, SideMenu) -> $scope.Enterprise = Enterprise.enterprise $scope.menu = SideMenu $scope.select = SideMenu.select @@ -12,10 +12,23 @@ angular.module("admin.enterprises") { name: 'About', icon_class: "icon-pencil" } { name: 'Business Details', icon_class: "icon-briefcase" } { name: 'Images', icon_class: "icon-picture" } - { name: "Shipping Methods", icon_class: "icon-truck" } - { name: "Payment Methods", icon_class: "icon-money" } - { name: "Enterprise Fees", icon_class: "icon-tasks" } - { name: "Shop Preferences", icon_class: "icon-shopping-cart" } + { name: "Shipping Methods", icon_class: "icon-truck", show: "showExtraPrefs()" } + { name: "Payment Methods", icon_class: "icon-money", show: "showExtraPrefs()" } + { name: "Enterprise Fees", icon_class: "icon-tasks", show: "showEnterpriseFees()" } + { name: "Shop Preferences", icon_class: "icon-shopping-cart", show: "showExtraPrefs()" } ] $scope.select(0) + + + $scope.showItem = (item) -> + if item.show? + $parse(item.show)($scope) + else + true + + $scope.showExtraPrefs = -> + $scope.Enterprise.sells != "none" + + $scope.showEnterpriseFees = -> + $scope.Enterprise.sells != "none" || $scope.Enterprise.is_primary_producer diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/enterprises/_side_menu.html.haml index 383d5181fb..ff5d086570 100644 --- a/app/views/admin/enterprises/_side_menu.html.haml +++ b/app/views/admin/enterprises/_side_menu.html.haml @@ -2,8 +2,9 @@ %a.menu_item{ href: "", id: "{{ item.name.toLowerCase().replace(' ', '_') }}", ng: { repeat: '(index,item) in menu.items | filter:{visible:true}', click: 'select(index)', + show: 'showItem(item)', class: '{ selected: item.selected}', 'class-odd' => "'odd'", 'class-even' => "'even'" } } %i{ class: "{{item.icon_class}}" } - %span {{ item.name }} \ No newline at end of file + %span {{ item.name }} From dcc04ea538a69ebfe60422e504b0d8210d145a00 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 15 Jan 2015 12:39:16 +1100 Subject: [PATCH 501/681] On enterprise edit page, do not show side menu links we don't have permission to --- .../side_menu_controller.js.coffee | 21 ++++++++++++------- app/helpers/admin/injection_helper.rb | 9 ++++++++ .../admin/enterprises/_form_data.html.haml | 3 ++- spec/features/admin/enterprises_spec.rb | 15 +++++++++++-- .../side_menu_controller_spec.js.coffee | 2 +- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index dddaa1a37d..85a15ebdf3 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -1,5 +1,5 @@ angular.module("admin.enterprises") - .controller "sideMenuCtrl", ($scope, $parse, Enterprise, SideMenu) -> + .controller "sideMenuCtrl", ($scope, $parse, Enterprise, SideMenu, enterprisePermissions) -> $scope.Enterprise = Enterprise.enterprise $scope.menu = SideMenu $scope.select = SideMenu.select @@ -12,10 +12,10 @@ angular.module("admin.enterprises") { name: 'About', icon_class: "icon-pencil" } { name: 'Business Details', icon_class: "icon-briefcase" } { name: 'Images', icon_class: "icon-picture" } - { name: "Shipping Methods", icon_class: "icon-truck", show: "showExtraPrefs()" } - { name: "Payment Methods", icon_class: "icon-money", show: "showExtraPrefs()" } + { name: "Shipping Methods", icon_class: "icon-truck", show: "showShippingMethods()" } + { name: "Payment Methods", icon_class: "icon-money", show: "showPaymentMethods()" } { name: "Enterprise Fees", icon_class: "icon-tasks", show: "showEnterpriseFees()" } - { name: "Shop Preferences", icon_class: "icon-shopping-cart", show: "showExtraPrefs()" } + { name: "Shop Preferences", icon_class: "icon-shopping-cart", show: "showShopPreferences()" } ] $scope.select(0) @@ -27,8 +27,15 @@ angular.module("admin.enterprises") else true - $scope.showExtraPrefs = -> - $scope.Enterprise.sells != "none" + $scope.showShippingMethods = -> + enterprisePermissions.can_manage_shipping_methods && $scope.Enterprise.sells != "none" + + $scope.showPaymentMethods = -> + enterprisePermissions.can_manage_payment_methods && $scope.Enterprise.sells != "none" $scope.showEnterpriseFees = -> - $scope.Enterprise.sells != "none" || $scope.Enterprise.is_primary_producer + enterprisePermissions.can_manage_enterprise_fees && ($scope.Enterprise.sells != "none" || $scope.Enterprise.is_primary_producer) + + $scope.showShopPreferences = -> + $scope.Enterprise.sells != "none" + diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index b35e2ab437..50b9aa1125 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -33,6 +33,15 @@ module Admin admin_inject_json_ams_array "ofn.admin", "producers", @producers, Api::Admin::IdNameSerializer end + def admin_inject_enterprise_permissions + permissions = + {can_manage_shipping_methods: can?(:manage_shipping_methods, @enterprise), + can_manage_payment_methods: can?(:manage_payment_methods, @enterprise), + can_manage_enterprise_fees: can?(:manage_enterprise_fees, @enterprise)} + + render partial: "admin/json/injection_ams", locals: {ngModule: "admin.enterprises", name: "enterprisePermissions", json: permissions.to_json} + end + def admin_inject_hub_permissions render partial: "admin/json/injection_ams", locals: {ngModule: "ofn.admin", name: "hubPermissions", json: @hub_permissions.to_json} end diff --git a/app/views/admin/enterprises/_form_data.html.haml b/app/views/admin/enterprises/_form_data.html.haml index 0a40538533..ee3d17d4aa 100644 --- a/app/views/admin/enterprises/_form_data.html.haml +++ b/app/views/admin/enterprises/_form_data.html.haml @@ -1,4 +1,5 @@ = admin_inject_enterprise = admin_inject_taxons = admin_inject_payment_methods -= admin_inject_shipping_methods \ No newline at end of file += admin_inject_shipping_methods += admin_inject_enterprise_permissions diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index dfd7afb6aa..836a408bf3 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -442,11 +442,22 @@ feature %q{ distributor3.reload.name.should == 'Eaterprises' end - it "does not show links to manage payment methods, shipping methods or enterprise fees" do + it "does not show links to manage shipping methods, payment methods or enterprise fees" do click_link 'Enterprises' within("#listing_enterprises tr.enterprise-#{distributor3.id}") do - page.should_not have_link 'Payment Methods' page.should_not have_link 'Shipping Methods' + page.should_not have_link 'Payment Methods' + page.should_not have_link 'Enterprise Fees' + end + end + + it "does not show links to manage shipping methods, payment methods or enterprise fees on the edit page", js: true do + click_link 'Enterprises' + within("#listing_enterprises tr.enterprise-#{distributor3.id}") { click_link 'Edit Profile' } + + within(".side_menu") do + page.should_not have_link 'Shipping Methods' + page.should_not have_link 'Payment Methods' page.should_not have_link 'Enterprise Fees' end end diff --git a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee index 136203a644..ebed830c6b 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee @@ -20,7 +20,7 @@ describe "menuCtrl", -> SideMenu = _SideMenu_ spyOn(SideMenu, "select").andCallThrough() spyOn(SideMenu, "setItems").andCallThrough() - ctrl = $controller 'sideMenuCtrl', {$scope: scope, Enterprise: Enterprise, SideMenu: SideMenu} + ctrl = $controller 'sideMenuCtrl', {$scope: scope, Enterprise: Enterprise, SideMenu: SideMenu, enterprisePermissions: {}} describe "initialisation", -> it "stores enterprise", -> From 4dc0701213d6eb88e77263423e575fb97c19b327 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 15 Jan 2015 15:04:21 +1100 Subject: [PATCH 502/681] Side menu for enterprise group page --- app/assets/javascripts/admin/all.js | 1 + .../enterprise_group_controller.js.coffee | 3 ++ .../side_menu_controller.js.coffee | 13 +++++ .../enterprise_groups.js.coffee | 1 + .../admin/enterprise_groups/_form.html.haml | 52 ++++--------------- .../admin/enterprise_groups/_inputs.html.haml | 52 +++++++++++++++++++ .../admin/enterprise_groups/edit.html.haml | 4 +- .../admin/enterprise_groups/new.html.haml | 4 +- .../admin/enterprises/_ng_form.html.haml | 2 +- .../_side_menu.html.haml | 0 10 files changed, 83 insertions(+), 49 deletions(-) create mode 100644 app/assets/javascripts/admin/enterprise_groups/controllers/enterprise_group_controller.js.coffee create mode 100644 app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee create mode 100644 app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee create mode 100644 app/views/admin/enterprise_groups/_inputs.html.haml rename app/views/admin/{enterprises => shared}/_side_menu.html.haml (100%) diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 78ff8306f8..9a07309352 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -19,6 +19,7 @@ //= require ../shared/ng-infinite-scroll.min.js //= require ./admin //= require ./enterprises/enterprises +//= require ./enterprise_groups/enterprise_groups //= require ./payment_methods/payment_methods //= require ./products/products //= require ./shipping_methods/shipping_methods diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/enterprise_group_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/enterprise_group_controller.js.coffee new file mode 100644 index 0000000000..c207032853 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/enterprise_group_controller.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.enterprise_groups") + .controller "enterpriseGroupCtrl", ($scope, SideMenu) -> + $scope.menu = SideMenu diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee new file mode 100644 index 0000000000..652f06e426 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee @@ -0,0 +1,13 @@ +angular.module("admin.enterprise_groups") + .controller "sideMenuCtrl", ($scope, SideMenu) -> + $scope.menu = SideMenu + $scope.select = SideMenu.select + + $scope.menu.setItems [ + { name: 'Primary Details', icon_class: "icon-user" } + { name: 'About', icon_class: "icon-pencil" } + { name: 'Images', icon_class: "icon-picture" } + { name: 'Contact', icon_class: "icon-phone" } + ] + + $scope.select(0) diff --git a/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee b/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee new file mode 100644 index 0000000000..2d23b9c6fa --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee @@ -0,0 +1 @@ +angular.module("admin.enterprise_groups", ["admin.side_menu"]) diff --git a/app/views/admin/enterprise_groups/_form.html.haml b/app/views/admin/enterprise_groups/_form.html.haml index ec4934bf19..9ebf6db147 100644 --- a/app/views/admin/enterprise_groups/_form.html.haml +++ b/app/views/admin/enterprise_groups/_form.html.haml @@ -1,43 +1,11 @@ -= f.field_container :name do - = f.label :name - %br/ - = f.text_field :name += render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise } -= 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/ - = f.check_box :on_front_page - -= f.field_container :enterprise_ids do - = 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 += form_for [main_app, :admin, @enterprise_group] do |f| + .row{ ng: {app: 'admin.enterprise_groups', controller: 'enterpriseGroupCtrl'} } + .sixteen.columns.alpha + .four.columns.alpha + = render 'admin/shared/side_menu' + .one.column   + .eleven.columns.omega.fullwidth_inputs + = render :partial => 'inputs', :locals => { :f => f } + = render partial: "spree/admin/shared/#{action}_resource_links" diff --git a/app/views/admin/enterprise_groups/_inputs.html.haml b/app/views/admin/enterprise_groups/_inputs.html.haml new file mode 100644 index 0000000000..d3baec4a0c --- /dev/null +++ b/app/views/admin/enterprise_groups/_inputs.html.haml @@ -0,0 +1,52 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Primary Details'" } } + %legend Primary Details + = f.field_container :name do + = f.label :name + %br/ + = f.text_field :name + + = f.field_container :description do + = f.label :description + %br/ + = f.text_field :description + + = f.field_container :on_front_page do + = f.label :on_front_page, 'On front page?' + %br/ + = f.check_box :on_front_page + + = f.field_container :enterprise_ids do + = f.label :enterprise_ids, 'Enterprises' + %br/ + = f.collection_select :enterprise_ids, Enterprise.all, :id, :name, {}, {class: "select2 fullwidth", multiple: true} + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } + %legend About + = f.field_container :long_description do + = f.label :long_description + %br/ + = f.text_area :long_description + + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Images'" } } + %legend Images + .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 + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } + %legend Contact + TODO diff --git a/app/views/admin/enterprise_groups/edit.html.haml b/app/views/admin/enterprise_groups/edit.html.haml index 19c954f8e4..d83367b8c5 100644 --- a/app/views/admin/enterprise_groups/edit.html.haml +++ b/app/views/admin/enterprise_groups/edit.html.haml @@ -1,5 +1,3 @@ = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise } -= form_for [main_app, :admin, @enterprise_group] do |f| - = render :partial => 'form', :locals => { :f => f } - = render :partial => 'spree/admin/shared/edit_resource_links' += render 'admin/enterprise_groups/form', action: 'edit' diff --git a/app/views/admin/enterprise_groups/new.html.haml b/app/views/admin/enterprise_groups/new.html.haml index f899eaa380..73d597a920 100644 --- a/app/views/admin/enterprise_groups/new.html.haml +++ b/app/views/admin/enterprise_groups/new.html.haml @@ -1,5 +1,3 @@ = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise } -= form_for [main_app, :admin, @enterprise_group] do |f| - = render :partial => 'form', :locals => { :f => f } - = render :partial => 'spree/admin/shared/new_resource_links' += render 'admin/enterprise_groups/form', action: 'new' diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 76ec768a3a..fa2d12aeae 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -12,7 +12,7 @@ .row .sixteen.columns.alpha .four.columns.alpha - = render 'side_menu' + = render 'admin/shared/side_menu' .one.column   .eleven.columns.omega.fullwidth_inputs = render 'form', f: f diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/shared/_side_menu.html.haml similarity index 100% rename from app/views/admin/enterprises/_side_menu.html.haml rename to app/views/admin/shared/_side_menu.html.haml From 891a9b06a8bf7d8e0e52c660c0dab73bf1092255 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 15:23:10 +1100 Subject: [PATCH 503/681] Add expand icon on top of image thumbnail --- app/views/shop/products/_summary.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml index 2a8430ec14..fc29c8e061 100644 --- a/app/views/shop/products/_summary.html.haml +++ b/app/views/shop/products/_summary.html.haml @@ -1,5 +1,6 @@ .product-thumb %a{"ng-click" => "triggerProductModal()"} + %i.ofn-i_057-expand %img{"bo-src" => "product.primaryImageOrMissing", "ng-click" => "triggerProductModal()"} .row.summary From 9b258e075b26103d796f5072503c18bdac34a341 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 15:23:36 +1100 Subject: [PATCH 504/681] Add styling and animation to img thumbnail --- .../darkswarm/_shop-product-thumb.css.sass | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass index 7a69707a60..b26a278a76 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass @@ -1,9 +1,11 @@ @import branding +@import animations.sass .darkswarm products product .product-thumb + @include csstrans position: absolute top: 3px left: 0px @@ -15,7 +17,32 @@ z-index: 999999 background-color: white overflow: hidden - + i + @include csstrans + transition-delay: 150ms + position: absolute + left: 45% + top: 45% + z-index: 99999 + color: white + font-size: 1rem + opacity: 0 + img + @include csstrans + opacity: 1 + @include transform-scale(scale(1)) + + &:hover, &:focus, &:active + background-color: $clr-brick + i + left: 32% + top: 30% + font-size: 3rem + opacity: 1 + img + opacity: 0.5 + @include transform-scale(scale(1.1)) + @media all and (max-width: 768px) width: 4rem height: 4rem From 51c8891faca52177c26ce3a0a24afabf9e7b1753 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 15:24:01 +1100 Subject: [PATCH 505/681] Add animation to product title hover --- .../stylesheets/darkswarm/_shop-product-rows.css.sass | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index d5d29fc23b..14406b46eb 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -1,4 +1,5 @@ @import branding.css.sass +@import animations.sass .darkswarm products @@ -107,10 +108,11 @@ margin: 0 h3 a color: #222 - &:hover, &:focus, &:active - color: $clr-brick i + @include csstrans font-size: 0.6em - - + &:hover, &:focus, &:active + color: $clr-brick + i + font-size: 0.8em From 4d7871a0bb5ec4de127e3166b8c132ca399fe656 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:00:21 +1100 Subject: [PATCH 506/681] New grey color variable --- app/assets/stylesheets/darkswarm/branding.css.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/darkswarm/branding.css.sass b/app/assets/stylesheets/darkswarm/branding.css.sass index 0fa559240f..d5d319cb2e 100644 --- a/app/assets/stylesheets/darkswarm/branding.css.sass +++ b/app/assets/stylesheets/darkswarm/branding.css.sass @@ -21,5 +21,6 @@ $disabled-med: #b3b3b3 $disabled-dark: #999 $disabled-v-dark: #808080 $med-grey: #666 +$med-drk-grey: #444 $dark-grey: #333 $black: #000 From 932604bd69f7d6f53164dd1b6d6ccc25d74a42c4 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:01:00 +1100 Subject: [PATCH 507/681] Add in logic to style price column differently if nul vs has value --- app/assets/javascripts/templates/shop_variant.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index b1538bb69e..6d050d0bde 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -58,5 +58,5 @@ .small-12.medium-2.large-2.columns.total-price.text-right .table-cell - %strong + %strong{"ng-class" => "{filled: variant.totalPrice()}"} {{ variant.totalPrice() | localizeCurrency }} From 47a8731b8ecd2423fd4526c69c3fc6ed31fd1db1 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:01:49 +1100 Subject: [PATCH 508/681] Styling for price column to distinguish between product added vs null --- app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index 14406b46eb..b7ca3da2db 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -72,6 +72,9 @@ // Total price .total-price padding-left: 0rem + color: $disabled-med + .filled + color: $med-drk-grey @media all and (max-width: 640px) background: #777 color: white From 25a734b20808650e6fd91d269e9b79662dfbdf09 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 15 Jan 2015 16:08:03 +1100 Subject: [PATCH 509/681] When creating enterprise, establish relationships with the owner's hubs --- app/models/enterprise.rb | 16 +++++++++++++++- spec/models/enterprise_spec.rb | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 647b7a2990..929fcd1a7e 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -60,12 +60,13 @@ class Enterprise < ActiveRecord::Base validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? } validates_length_of :description, :maximum => 255 - before_save :confirmation_check, if: lambda{ email_changed? } + before_save :confirmation_check, if: lambda { email_changed? } before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? } before_validation :set_unused_address_fields after_validation :geocode_address + after_create :relate_to_owners_hubs # TODO: Later versions of devise have a dedicated after_confirmation callback, so use that after_update :welcome_after_confirm, if: lambda { confirmation_token_changed? && confirmation_token.nil? } after_create :send_welcome_email, if: lambda { email_is_known? } @@ -93,6 +94,7 @@ class Enterprise < ActiveRecord::Base } scope :is_primary_producer, where(:is_primary_producer => true) scope :is_distributor, where('sells != ?', 'none') + scope :is_hub, where(sells: 'any') scope :supplying_variant_in, lambda { |variants| joins(:supplied_products => :variants_including_master).where('spree_variants.id IN (?)', variants).select('DISTINCT enterprises.*') } scope :with_supplied_active_products_on_hand, lambda { joins(:supplied_products) @@ -343,6 +345,18 @@ class Enterprise < ActiveRecord::Base end end + def relate_to_owners_hubs + hubs = owner.owned_enterprises.is_hub.where('enterprises.id != ?', self) + + hubs.each do |hub| + EnterpriseRelationship.create!(parent: self, + child: hub, + permissions_list: [:add_to_order_cycle, + :manage_products, + :edit_profile]) + end + end + def shopfront_taxons unless preferred_shopfront_taxon_order =~ /\A((\d+,)*\d+)?\z/ errors.add(:shopfront_category_ordering, "must contain a list of taxons.") diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index b65ab8b8e4..37535a2c3c 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -580,6 +580,41 @@ describe Enterprise do end end + describe "callbacks" do + describe "after creation" do + let(:owner) { create(:user, enterprise_limit: 10) } + + let(:hub1) { create(:distributor_enterprise, owner: owner) } + let(:hub2) { create(:distributor_enterprise, owner: owner) } + let(:producer) { create(:supplier_enterprise, owner: owner) } + + let(:er1) { EnterpriseRelationship.where(child_id: hub1).last } + let(:er2) { EnterpriseRelationship.where(child_id: hub2).last } + + it "establishes relationships with the owner's hubs" do + hub1 + hub2 + enterprise = nil + + expect do + enterprise = create(:enterprise, owner: owner) + end.to change(EnterpriseRelationship, :count).by(2) + + [er1, er2].each do |er| + er.parent.should == enterprise + er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile'].sort + end + end + + it "doesn't relate to enterprises that aren't sells=='any'" do + producer + expect do + enterprise = create(:enterprise, owner: owner) + end.to change(EnterpriseRelationship, :count).by(0) + end + end + end + describe "has_supplied_products_on_hand?" do before :each do @supplier = create(:supplier_enterprise) From 9db1c4c708bf49df70663550d4d465a2c13b8ad3 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:15:23 +1100 Subject: [PATCH 510/681] Adding in new color variable --- app/assets/stylesheets/darkswarm/active_table_search.css.sass | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/active_table_search.css.sass b/app/assets/stylesheets/darkswarm/active_table_search.css.sass index 4d9f0d0e4e..635d2e2e4b 100644 --- a/app/assets/stylesheets/darkswarm/active_table_search.css.sass +++ b/app/assets/stylesheets/darkswarm/active_table_search.css.sass @@ -66,10 +66,8 @@ products .filter-box table-layout: fixed text-transform: capitalize overflow: visible - // width: 100% - // height: 2rem line-height: 1 - color: #444 + color: $med-drk-grey font-size: 0.875rem span display: table-cell From 1dfbc8881324438a666cb76bfe7e044726a174f8 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:16:01 +1100 Subject: [PATCH 511/681] Change logic on disabled class on Your shopping cart button as this was swapped the wrong way around --- app/views/shop/products/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 01e3b5ef19..0574822129 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -13,7 +13,7 @@ %form{action: cart_path} .small-12.medium-4.large-3.columns %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", - "ng-disabled" => "Cart.dirty"} + "ng-disabled" => "!Cart.dirty"} %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", @@ -42,5 +42,5 @@ .small-12.columns %form{action: cart_path} %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", - "ng-disabled" => "Cart.dirty"} + "ng-disabled" => "!Cart.dirty"} From 676e7cb4d6237ddd0a62cbafd8e24c381bbe0215 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:45:09 +1100 Subject: [PATCH 512/681] Make taxon flag 2 cols for small devices --- app/views/shop/products/_summary.html.haml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml index fc29c8e061..392d7ef31d 100644 --- a/app/views/shop/products/_summary.html.haml +++ b/app/views/shop/products/_summary.html.haml @@ -4,19 +4,17 @@ %img{"bo-src" => "product.primaryImageOrMissing", "ng-click" => "triggerProductModal()"} .row.summary - .small-9.medium-10.large-11.columns.summary-header + .small-10.medium-10.large-11.columns.summary-header %h3 %a{"ng-click" => "triggerProductModal()"} {{ product.name }} %i.ofn-i_057-expand - %small %em from %span %enterprise-modal %i.ofn-i_036-producers {{ enterprise.name }} - - .small-3.medium-2.large-1.columns.text-center + .small-2.medium-2.large-1.columns.text-center .taxon-flag %render-svg{path: "{{product.primary_taxon.icon}}"} From f8622be21deef11e0b0c071a111edf60fb3d2a29 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:45:29 +1100 Subject: [PATCH 513/681] Adding in new color variable --- app/assets/stylesheets/darkswarm/shop.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 3482195d14..6dd03fce3d 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -33,7 +33,7 @@ margin-bottom: 20px !important position: relative display: block - color: #444 + color: $med-drk-grey &:hover, &:focus, &:active border-bottom: 1px solid $clr-brick-med-bright From d5d218c450f31cab5e9710c4fceaf90e7537a66c Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:45:54 +1100 Subject: [PATCH 514/681] Make taxon flag smaller for smallest device breakpoint --- .../darkswarm/_shop-taxon-flag.css.sass | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-taxon-flag.css.sass b/app/assets/stylesheets/darkswarm/_shop-taxon-flag.css.sass index b37606c43f..7a6b04cbc2 100644 --- a/app/assets/stylesheets/darkswarm/_shop-taxon-flag.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-taxon-flag.css.sass @@ -9,9 +9,10 @@ margin-top: -1.1rem padding-top: 0.25rem z-index: 999999 - - @media all and (max-width: 768px) - margin-top: -0.85rem + @media all and (max-width: 480px) + background-size: 28px 32px + min-height: 32px + width: 28px render-svg svg @@ -19,3 +20,12 @@ height: 24px path fill: #999 + + @media all and (max-width: 768px) + margin-top: -0.85rem + + @media all and (max-width: 480px) + render-svg + svg + width: 18px + height: 18px From 4a881a5aa569e7cf29d8368c6bfce9cbbb44b831 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:46:18 +1100 Subject: [PATCH 515/681] Turn off product thumb at a larger break point --- .../darkswarm/_shop-product-thumb.css.sass | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass index b26a278a76..e4621f9fa1 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass @@ -44,10 +44,17 @@ @include transform-scale(scale(1.1)) @media all and (max-width: 768px) + top: 2px width: 4rem height: 4rem - - @media all and (max-width: 480px) + &:hover, &:focus, &:active + i + left: 30% + top: 30% + font-size: 2rem + @media all and (max-width: 640px) display: none width: 0rem - height: 0rem \ No newline at end of file + height: 0rem + + \ No newline at end of file From cdd6c2daf29ccdb0414b4daa1da7cdba454d4540 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 16:46:55 +1100 Subject: [PATCH 516/681] Styling product summary row to allow for hiding product thumb at a larger break point --- .../stylesheets/darkswarm/_shop-product-rows.css.sass | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index b7ca3da2db..c67b030292 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -77,7 +77,9 @@ color: $med-drk-grey @media all and (max-width: 640px) background: #777 - color: white + color: $disabled-med + .filled + color: white .table-cell height: 27px @@ -101,8 +103,6 @@ @media all and (max-width: 768px) padding-left: 4.9375rem @media all and (max-width: 640px) - padding-left: 4.5rem - @media all and (max-width:480px) padding-left: 0.9375rem small font-size: 80% From d2277999cc2543a47bc4e6bc4aa5d7f9605ed2ca Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 17:13:10 +1100 Subject: [PATCH 517/681] Undo change of logic on disabled state on Shopping cart button --- app/views/shop/products/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 0574822129..01e3b5ef19 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -13,7 +13,7 @@ %form{action: cart_path} .small-12.medium-4.large-3.columns %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", - "ng-disabled" => "!Cart.dirty"} + "ng-disabled" => "Cart.dirty"} %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", @@ -42,5 +42,5 @@ .small-12.columns %form{action: cart_path} %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", - "ng-disabled" => "!Cart.dirty"} + "ng-disabled" => "Cart.dirty"} From ac34da4f241713e9810746ada8ed273433a2d0d3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 15 Jan 2015 17:19:48 +1100 Subject: [PATCH 518/681] Altering logic around disabled state for 'Your Shopping Cart' button --- app/assets/javascripts/darkswarm/services/cart.js.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index 2b58bb70ae..98df66f592 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -8,6 +8,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> for line_item in @line_items line_item.variant.line_item = line_item Variants.register line_item.variant + @dirty = @empty() orderChanged: => @unsaved() @@ -31,7 +32,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> saved: => - @dirty = false + @dirty = @empty() $(window).unbind "beforeunload" unsaved: => @@ -62,4 +63,4 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> variant: variant quantity: null max_quantity: null - @line_items.push variant.line_item + @line_items.push variant.line_item \ No newline at end of file From b3e67fa1640721d66b0f8f28658eda12204d07bd Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 15 Jan 2015 17:34:04 +1100 Subject: [PATCH 519/681] Take two on disabled logic --- .../javascripts/darkswarm/services/cart.js.coffee | 3 +-- app/views/shared/menu/_cart.html.haml | 14 +++++++------- app/views/shop/products/_form.html.haml | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index 98df66f592..74f978bf3a 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -8,7 +8,6 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> for line_item in @line_items line_item.variant.line_item = line_item Variants.register line_item.variant - @dirty = @empty() orderChanged: => @unsaved() @@ -32,7 +31,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> saved: => - @dirty = @empty() + @dirty = false $(window).unbind "beforeunload" unsaved: => diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index 74f9ee2b29..dda3149ea2 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -1,6 +1,6 @@ -%span.cart-span{"ng-controller" => "CartCtrl", "ng-class" => "{ dirty: Cart.dirty }"} +%span.cart-span{"ng-controller" => "CartCtrl", "ng-class" => "{ dirty: Cart.dirty || Cart.empty() }"} %a#cart.icon{cart: true} - %span.nav-branded + %span.nav-branded %i.ofn-i_027-shopping-cart %span {{ Cart.line_items_present().length }} @@ -23,19 +23,19 @@ %span.quantity {{ line_item.quantity }} %i.ofn-i_009-close %span.price {{ line_item.variant.price_with_fees | localizeCurrency }} - + .columns.small-2 %small \= - %strong + %strong .total-price.right {{ line_item.variant.totalPrice() | localizeCurrency }} %li.total-cart{"ng-show" => "Cart.line_items_present().length > 0"} .row .columns.small-6 - %em Total: + %em Total: .columns.small-6.text-right - %strong {{ Cart.total() | localizeCurrency }} + %strong {{ Cart.total() | localizeCurrency }} .text-right - %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty"} Quick checkout + %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"} Quick checkout diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 01e3b5ef19..252d810bab 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -13,7 +13,7 @@ %form{action: cart_path} .small-12.medium-4.large-3.columns %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", - "ng-disabled" => "Cart.dirty"} + "ng-disabled" => "Cart.dirty || Cart.empty()"} %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", @@ -42,5 +42,5 @@ .small-12.columns %form{action: cart_path} %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", - "ng-disabled" => "Cart.dirty"} + "ng-disabled" => "Cart.dirty || Cart.empty()"} From d2d3a577ea602e14d4533f2ec66c7016c337ec6e Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 15 Jan 2015 17:38:35 +1100 Subject: [PATCH 520/681] Merging master into this branch to make it easy to pull for staging push --- app/models/enterprise.rb | 16 +++++++++++++++- spec/models/enterprise_spec.rb | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 647b7a2990..929fcd1a7e 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -60,12 +60,13 @@ class Enterprise < ActiveRecord::Base validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? } validates_length_of :description, :maximum => 255 - before_save :confirmation_check, if: lambda{ email_changed? } + before_save :confirmation_check, if: lambda { email_changed? } before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? } before_validation :set_unused_address_fields after_validation :geocode_address + after_create :relate_to_owners_hubs # TODO: Later versions of devise have a dedicated after_confirmation callback, so use that after_update :welcome_after_confirm, if: lambda { confirmation_token_changed? && confirmation_token.nil? } after_create :send_welcome_email, if: lambda { email_is_known? } @@ -93,6 +94,7 @@ class Enterprise < ActiveRecord::Base } scope :is_primary_producer, where(:is_primary_producer => true) scope :is_distributor, where('sells != ?', 'none') + scope :is_hub, where(sells: 'any') scope :supplying_variant_in, lambda { |variants| joins(:supplied_products => :variants_including_master).where('spree_variants.id IN (?)', variants).select('DISTINCT enterprises.*') } scope :with_supplied_active_products_on_hand, lambda { joins(:supplied_products) @@ -343,6 +345,18 @@ class Enterprise < ActiveRecord::Base end end + def relate_to_owners_hubs + hubs = owner.owned_enterprises.is_hub.where('enterprises.id != ?', self) + + hubs.each do |hub| + EnterpriseRelationship.create!(parent: self, + child: hub, + permissions_list: [:add_to_order_cycle, + :manage_products, + :edit_profile]) + end + end + def shopfront_taxons unless preferred_shopfront_taxon_order =~ /\A((\d+,)*\d+)?\z/ errors.add(:shopfront_category_ordering, "must contain a list of taxons.") diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index b65ab8b8e4..37535a2c3c 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -580,6 +580,41 @@ describe Enterprise do end end + describe "callbacks" do + describe "after creation" do + let(:owner) { create(:user, enterprise_limit: 10) } + + let(:hub1) { create(:distributor_enterprise, owner: owner) } + let(:hub2) { create(:distributor_enterprise, owner: owner) } + let(:producer) { create(:supplier_enterprise, owner: owner) } + + let(:er1) { EnterpriseRelationship.where(child_id: hub1).last } + let(:er2) { EnterpriseRelationship.where(child_id: hub2).last } + + it "establishes relationships with the owner's hubs" do + hub1 + hub2 + enterprise = nil + + expect do + enterprise = create(:enterprise, owner: owner) + end.to change(EnterpriseRelationship, :count).by(2) + + [er1, er2].each do |er| + er.parent.should == enterprise + er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile'].sort + end + end + + it "doesn't relate to enterprises that aren't sells=='any'" do + producer + expect do + enterprise = create(:enterprise, owner: owner) + end.to change(EnterpriseRelationship, :count).by(0) + end + end + end + describe "has_supplied_products_on_hand?" do before :each do @supplier = create(:supplier_enterprise) From b451b94fd1b21cfacbe528a557051961a238955d Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 13:06:02 +1100 Subject: [PATCH 521/681] Improving styling on large thumbnail view of product images --- app/assets/stylesheets/darkswarm/images.css.sass | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/images.css.sass b/app/assets/stylesheets/darkswarm/images.css.sass index f94240b4af..798e656481 100644 --- a/app/assets/stylesheets/darkswarm/images.css.sass +++ b/app/assets/stylesheets/darkswarm/images.css.sass @@ -3,12 +3,10 @@ @import branding .product-img - border-bottom: 40px white solid - border-top: 20px white solid - border-left: 20px white solid - border-right: 20px white solid + padding: 5px + margin-bottom: 10px outline: 1px solid #ccc - @include box-shadow(0 1px 2px 1px rgba(0,0,0,0.25)) + @include box-shadow(0 1px 2px 1px rgba(0,0,0,0.15)) .hero-img outline: 1px solid $disabled-bright From 2a2188921660b5edfbeaecb13807f81f8e9d22b5 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 13:06:30 +1100 Subject: [PATCH 522/681] Remove border on product thumbnail to clean up UI --- app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass index e4621f9fa1..b5dca200c5 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass @@ -7,11 +7,10 @@ .product-thumb @include csstrans position: absolute - top: 3px + top: 2px left: 0px width: 7rem height: 7rem - outline: 1px solid $disabled-bright float: left display: block z-index: 999999 From 21108b34b62c5233c64679f8bcdbb93e8f765478 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 13:26:40 +1100 Subject: [PATCH 523/681] Add new animation for spinning --- app/assets/stylesheets/darkswarm/animations.sass | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/animations.sass b/app/assets/stylesheets/darkswarm/animations.sass index 6532cce60d..544e2823ce 100644 --- a/app/assets/stylesheets/darkswarm/animations.sass +++ b/app/assets/stylesheets/darkswarm/animations.sass @@ -51,6 +51,21 @@ 100% opacity: 1 +@-webkit-keyframes spin + 0% + -webkit-transform: rotate(0deg) + transform: rotate(0deg) + 100% + -webkit-transform: rotate(359deg) + transform: rotate(359deg) +@keyframes spin + 0% + -webkit-transform: rotate(0deg) + transform: rotate(0deg) + 100% + -webkit-transform: rotate(359deg) + transform: rotate(359deg) + // ANIMATION CLASSES .fade From 51687b5c2ce662abdd3bbef3959066afeb824330 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 13:27:16 +1100 Subject: [PATCH 524/681] Tweak language and styling for the CTA buttons taking users to shopping cart and checkout pages --- app/views/shared/menu/_cart.html.haml | 3 ++- app/views/shop/products/_form.html.haml | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index dda3149ea2..28735b57f1 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -38,4 +38,5 @@ %strong {{ Cart.total() | localizeCurrency }} .text-right - %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"} Quick checkout + %input.small.button.secondary.add_to_cart{type: :submit, value: "Edit your cart", "ng-disabled" => "Cart.dirty || Cart.empty()" } + %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"} Checkout now diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 252d810bab..bfcb32845b 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -12,8 +12,8 @@ %form{action: cart_path} .small-12.medium-4.large-3.columns - %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", - "ng-disabled" => "Cart.dirty || Cart.empty()"} + %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} + %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : 'Edit your cart' }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", @@ -41,6 +41,6 @@ .row .small-12.columns %form{action: cart_path} - %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", - "ng-disabled" => "Cart.dirty || Cart.empty()"} + %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} + %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : 'Edit your cart' }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } From 8334ff736b20e6b7072d3002d77c6cdeeb998105 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 13:27:34 +1100 Subject: [PATCH 525/681] Styling for shopping cart buttons and spinner --- app/assets/stylesheets/darkswarm/shop.css.sass | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 6dd03fce3d..2ae0e809c7 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -1,4 +1,5 @@ @import mixins +@import animations @import variables @import branding @import big-input @@ -25,6 +26,21 @@ .add_to_cart margin-top: 2rem + form + input.small.button.primary.right.add_to_cart + &.dirty + padding-right: 3.2rem + i.cart-spinner + position: absolute + top: 14px + right: 38px + color: white + font-size: 1.2em + // Necessary to be below Z index of cart popover: + z-index: 98 + -webkit-animation: spin 2s infinite linear + animation: spin 2s infinite linear + product @include csstrans border-bottom: 1px solid #e5e5e5 From 3bd77c74a5fe701dbd728a9946ad5de111e90d74 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 14:57:09 +1100 Subject: [PATCH 526/681] Improve logic on shopping cart button to add meaningful labels for various states --- app/views/shared/menu/_cart.html.haml | 4 ++-- app/views/shop/products/_form.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index 28735b57f1..2729b2f08f 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -38,5 +38,5 @@ %strong {{ Cart.total() | localizeCurrency }} .text-right - %input.small.button.secondary.add_to_cart{type: :submit, value: "Edit your cart", "ng-disabled" => "Cart.dirty || Cart.empty()" } - %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"} Checkout now + %input.button.secondary.tiny.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } + %a.button.primary.tiny{href: checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"} Checkout now diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index bfcb32845b..bf1b519fde 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -13,7 +13,7 @@ %form{action: cart_path} .small-12.medium-4.large-3.columns %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} - %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : 'Edit your cart' }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } + %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", @@ -42,5 +42,5 @@ .small-12.columns %form{action: cart_path} %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} - %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : 'Edit your cart' }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } + %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } From 23fc428d95479cc2977942d1e6a0e9eea73c43e7 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 14:57:30 +1100 Subject: [PATCH 527/681] Move spinner icon to left of text --- app/assets/stylesheets/darkswarm/shop.css.sass | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 2ae0e809c7..89557fbbec 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -29,11 +29,11 @@ form input.small.button.primary.right.add_to_cart &.dirty - padding-right: 3.2rem + padding-left: 3.2rem i.cart-spinner position: absolute top: 14px - right: 38px + right: 146px color: white font-size: 1.2em // Necessary to be below Z index of cart popover: From 2c307f09c3e5f803245f6928f4dd341b74413fda Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 16 Jan 2015 15:47:56 +1100 Subject: [PATCH 528/681] Changing edit cart button to link --- app/views/shared/menu/_cart.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index 2729b2f08f..d9c990db5f 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -38,5 +38,6 @@ %strong {{ Cart.total() | localizeCurrency }} .text-right - %input.button.secondary.tiny.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } + %a.button.secondary.tiny.add_to_cart{ href: cart_path, type: :submit, "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } + {{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }} %a.button.primary.tiny{href: checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"} Checkout now From c0eb902eefa8009a71de0eecf7cf1585528871fb Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 16:00:18 +1100 Subject: [PATCH 529/681] Turn off text shadow for buttons on popover for top nav --- app/assets/stylesheets/darkswarm/menu.css.sass | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/menu.css.sass b/app/assets/stylesheets/darkswarm/menu.css.sass index 4c9dcd25c6..821c7a5fb4 100644 --- a/app/assets/stylesheets/darkswarm/menu.css.sass +++ b/app/assets/stylesheets/darkswarm/menu.css.sass @@ -8,9 +8,11 @@ nav @include textpress - + .joyride-tip-guide .button + text-shadow: none + + // Default overrides - big menu - .top-bar-section ul li.ofn-logo > a display: table-cell vertical-align: middle From d04e8433158afb82a95c18317b8d59f7aab6f723 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 16:01:17 +1100 Subject: [PATCH 530/681] kill unused line --- app/assets/stylesheets/darkswarm/menu.css.sass | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/menu.css.sass b/app/assets/stylesheets/darkswarm/menu.css.sass index 821c7a5fb4..b256c6165c 100644 --- a/app/assets/stylesheets/darkswarm/menu.css.sass +++ b/app/assets/stylesheets/darkswarm/menu.css.sass @@ -11,7 +11,6 @@ nav .joyride-tip-guide .button text-shadow: none - // Default overrides - big menu .top-bar-section ul li.ofn-logo > a display: table-cell From 1d80bee595909433640e7fe87613cc89fc5bd354 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 16 Jan 2015 16:06:51 +1100 Subject: [PATCH 531/681] Cart item total reflects total number of items in the cart --- .../darkswarm/services/cart.js.coffee | 5 ++++ app/views/shared/menu/_cart.html.haml | 2 +- .../darkswarm/services/cart_spec.js.coffee | 25 ++++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index 74f978bf3a..c519d22fdd 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -43,6 +43,11 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> @line_items.filter (li)-> li.quantity > 0 + total_item_count: => + @line_items_present().reduce (sum,li) -> + sum = sum + li.quantity + , 0 + empty: => @line_items_present().length == 0 diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index d9c990db5f..646bd33425 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -3,7 +3,7 @@ %span.nav-branded %i.ofn-i_027-shopping-cart %span - {{ Cart.line_items_present().length }} + {{ Cart.total_item_count() }} items .joyride-tip-guide{"ng-class" => "{ in: open }", "ng-show" => "open"} diff --git a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee index 4079d70bcb..4d13ccc8b5 100644 --- a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee @@ -1,15 +1,17 @@ describe 'Cart service', -> Cart = null Variants = null - variant = {id: 1} - order = { - line_items: [ - variant: variant - ] - } + variant = null + order = null beforeEach -> module 'Darkswarm' + variant = {id: 1} + order = { + line_items: [ + variant: variant + ] + } angular.module('Darkswarm').value('currentOrder', order) inject ($injector)-> Variants = $injector.get("Variants") @@ -23,10 +25,15 @@ describe 'Cart service', -> it "creates and backreferences new line items if necessary", -> Cart.register_variant(v2 = {id: 2}) - expect(Cart.line_items[1].variant).toBe v2 - expect(Cart.line_items[1].variant.line_item).toBe Cart.line_items[1] + expect(Cart.line_items[1].variant).toBe v2 + expect(Cart.line_items[1].variant.line_item).toBe Cart.line_items[1] it "returns a list of items actually in the cart", -> expect(Cart.line_items_present()).toEqual [] order.line_items[0].quantity = 1 - expect(Cart.line_items_present().length).toEqual 1 + expect(Cart.line_items_present().length).toEqual + + it "sums the quantity of each line item for cart total", -> + expect(Cart.line_items_present()).toEqual [] + order.line_items[0].quantity = 2 + expect(Cart.total_item_count()).toEqual 2 From 1753432f36b6eb28aefaac441793ec89c372b6b2 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 16 Jan 2015 17:06:40 +1100 Subject: [PATCH 532/681] Adding new loading animation to replace ugly and large gif --- app/assets/images/loading.gif | Bin 44663 -> 0 bytes app/assets/images/spinning-circles.svg | 55 ++++++++++++++++++ .../registration/images/logo.html.haml | 2 +- .../registration/images/promo.html.haml | 2 +- app/views/shop/products/_form.html.haml | 4 +- .../admin/orders/bulk_management.html.haml | 2 +- .../products/bulk_edit/_indicators.html.haml | 2 +- 7 files changed, 61 insertions(+), 6 deletions(-) delete mode 100644 app/assets/images/loading.gif create mode 100755 app/assets/images/spinning-circles.svg diff --git a/app/assets/images/loading.gif b/app/assets/images/loading.gif deleted file mode 100644 index 31b6c2c752f0512bb67469b8a062302db1e9046e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44663 zcma&NRa6{Y6lPge6;imny9WXxXad1C1QI+1cXy}oqHuS2cbDMq5ZocSL-3^L@1E6b zdQH#6ocHs5?>_tcWaVW9g$w~e0B`^RKp+qg4-c1@mj?$2o12@9i;EKz6N7_;U0q#G zO-)r*RYgTbY5#K~BO`-@gPon7&CJaH+XDRm8#)Ariu6}?NeNZyx7<7+r2n?)p1{ax z0DJ%^7yt%5B0VAjkP*3z(I%GJ7Uk78r`-;m(=Jii;kV*Gmx!NX47{~uF(ZXeqc*}h zGo>yHNnl)&TsI6KTHvo*4<+u5>7z!nUfT{|v?d>wnzhSE@1E<64k`oBz@Xre&@d;E zUxb}BEG9NCJ|P|)nW6_y%*f2j&QXS^7KlgZmXwy2XCM_;e+;OoZ)j{X^QmdWZtm>r z{>g9C*6-LeG(6H-)jv@;Iz2NR-#5`VIJdmA>Z<*#24`(&ckgTdW?S+8$>|v{>=lrI`jQPp}IDBo~^mlk1j0wywmP9sgJ?A$dplokSzaXAfo8jl0P z$Y3<8XEP!wKr4v+9VmCH4 z(+pi{N5*Z@4=8dYcrOMZncWn~`iGL;xoB(X1|nqLT>mOzEE&^g1B^|sKOW0@^YL`7 z?Vb9xm!x&x!foUESnxMR41=B~dz4y+(QMzwyYoGu)2WQxU|!k`49f>0-7g}}T})`NE=LAca!^|fW%96DN!(E@L+{GDIP0{U#Q=ny&9gJAY#Az6S0`Y0W>4L1Hk22VWOAt~|#n@OhY zei~}L5B#x&3W8=;01PIDqb7ylu_Qc2 z%dBu6%A-(So=vH}+tFDnE$;+4;04M1Y!&Uyo@_OSkIxl>g5B>qsS=4P@^eN6^3D+T zFP`SZj)A7-`mno#;ZZPs>6XuDm`d9)#WCq}3ua*Ji7 z*Bj5L_&Bh#lVX?xOOc5#vPBTU*wp*MTuX4rY6D(XiR8=@u%EJAxPD4f3Ej zTjilWN*Tm=^D6x8uGa3!ar{Jz=E`IPdLj~sOI3N_2ba}GYy?boOKiE;G9}>r6W<<1 z-3{#4Ho&tU{=e(V93<%;wxCZOt$6QA^i!scZpA{jA~o+HNhK7h7iT%NsihGsX*Iv^ z4phCfwKl#mUVs;i2zrE1l$0u;HR)n@gb4~$s0GhE2`-*4RnDphcI!m2JMs26r+R>} zhzsyR_Z$1#TLUWU&}+G6iWieHU3W%Yl}cS=3_&u6=QJMZq5ewB+nE8Jzb#hgdv zF}*6+)qo#lRDT@2rhH^t#`bQ@00OMVyIzQDG5ZoSoBv7wWT35a+40WBN_Q7IvaJb} zv7p_9l}Ip4)jH%_Q=wTK{G2eY4SwfAf|wvlAes}0Ml#T1CS84a45{)ENR+@p$n;Th z8`{1ai{n|>_K?(U}22T@mAALe_CkOaU70?l+ z9|5_hY{SjI29m5K_q{`zMV5SBJKiM?DeaB1dN;6p+WkI_^@bOM>avNG&@)7|KWpP4 zYJwj-@>8xoH;#?g7@Nrw1-o3u-VlsKxQ-^Tl5H6M1H`~^SqfDCcOFtq`2#-(Rguu( z2K?E_m~(nY@@#ZEIxv%wj-_Gv`@Lf_CeJ>)opzvJx^mi()F*m-b7=-1XK>6!I#WS- zFd$dWYORrpLwS3w?N@xpyQ{)O(Z`>T$ZT@d>XR{ctHhGquR6OUzL;cT6 zz}3eSl@jwgaz{nxG(Z{)`BY2xmn}g`@H=zHK{WulXTD`*w~({K7?V70va*X`1Sl*HB+r#@D`4naF=O6|P5+Gn74y!i7JzKa|B4*d(MMeH)RLJC7%v$hFK@ONcea zE6p6BLKjEw#2kk41Z$k+z&I}%jS+x_sXkZl~Q|iyht}H$$ zm$`399oDjZB?dTDeN;cEeN`($&+7yHjH(6_DndT?O~)btcc&Ag)+h@C?k%PxkuF7R zEAlQ2&KAl@53$y|8Wu|eQ-8qL!xS5JGM{Reg9@&lUihxqtDA@0swmOLw}6s^Kb2xF zcf_B7dq~TMK;obuo8E6fgAK}%xp{!ZifA)xN{zdEmdp)f(=Td|hGSG<7ru#Ikb@?A z-M1|AU%I^nIGlYZ4UV4rN+{sE$6@r^8oG({_8-w211ttLqiT_BsGqF~Pyugahv%5# z0;zt&`Xn*3X}>jY)M)G^6rCR|D%uz0lmle#oYg2pl1_vA}9c>>-^m-JOp>H=IxHfr0Ca4ewP&DP^V@jxggT{=hrLAXtlgdItNy&*mMjN)&{d%$V74~E9 z3Ob|Rcv;2OVL()RL05Eh-eK)evics%If!1htE)AIlyF6dJ`gEi+3sYncOnE-hbH_t z^>c~%DDGl^bHuN6`LVS1E;nAPw^%iI3Q+R;Yxjbme{6_5g;oNF`g5i9%D(hiZM(PW0Is?oDK@7rG|Kp}Q%yCICaBFHQ$fiCTZ8HB z4H`&eO`Kst4SIWF(QPwCx%T=RZ>zj58Cns)wr_JmHeca>?pRe61W?4XpYE6G*O&1k_BU-tpgO&<@I;0e2#SjU5)7c-y`QDEk*Hyy9omdWZ62^>P>+KR7tnVfo zKmz?B2O=R3=_B!$RSmAoQ4l~F z1j9{+-MDh0=%2iXW8wTG}sc?-JNr+$dH zh@gHS!WkQ(ZVX?!f+HnMM4?5LI-(=fh7G~&X<0zvSj#jEoJ={o9X`XkWxY;8lupMe z{*g#F10UCG-KN_H*M?sg7 zQs-Z(q!_s|$k*`TLAA=GP!vmyg49@=VJKTHG0IgOvO^qZox0W@0oh#?Sf2WKDQa19 z+~Yws%C3f32$29!ESnJ7&sO;0QG9T7ge5`L9&7xFIAC!k!SEpAlX1d(Hc}2Y3z;WJ zWqAS})~GLJv`vqRh>GO5>JYdP8}=(KDRC&yv_8q7HE~}x%0S56a~V%uJNX*~ohvPw ztTGOAOOlaRg#E4RsUh_xsBkG7P%KQWYsDG4O0<+JJaeTt)j&Rl7?}1(i`HK)joUIc9|B)1!(ACoC>Lbd@HE~U z#obAZz463DbX)5FWy3r2jA|U_D0pJNAmcrRMgJkg1D#m(JTdgi!%xDQUdpVI74S=h z!hE%PY>zFd%JJv4{U_QODbgA7bO_qWf}h%0F|4S*%Zrd$L6H>j3DOx4RW z>hul;;xeVuUpzeEr5}Y$p9&JMkW2LnO45J%AWM}QhL!SN<fDTX1( zAurey{eYet5(~q)>o@GbrLM=Dt3jd_jRfdysD4Vt54di&*0U% z&y|d2MPgc&e$0S#ubRj7n&h zIIS1(Zg@m*_~=y6ioxQ51Q4WX{M1oR#_@q-t)b3?4PT^DX|1uNJlc`5K{1U4`Lq$C zA<~E$L2U?c6lVd7dp8+yG)k|Ps8OUnsy7*CG;?J%wbwQ2tu+V|JMA}j-YU*5C zofF!!*Qztd+q7@n3NxznINCp|x0mZymqfHPH?-GwR8*a|V-s}LiF7o2ziGzkXwB%@ zs_&>+>*&_y?5Sw)6X~p0>n!r_9MNSPQ*57D>-4$l%%SL-<6vGmZCmv2l8^67>*!j? zVAzak+otFyc-x(z+kK!&bHveh($T#?+a2-JeWgovQ_*^-`?JOIXHdq^XK!-ED@SY6 z%TFWxY9z{*s0xW>>*hG`9!uR;Vuhakz!q$qCZ7s&KaoxY)m}@*7JN+K@UdRa@#gZK z<}hz2dYh*8#vTcIphZTXpkklxORtg+FdU;_AfrcZpij>eC>Ys*HrP)b4kUc-r}ge7 z?rT_AY>*MHm(!zHSnp(C8<53R|8mw5@aYu;1%2lyW3cbR<}BB-CdFmN~K;U&&uK zawHWmST+phOb`trGJZ+0TPFrpRK;?prDVQMn@HR|iL7cDs(c>MYcNNIp^GYXj!mG) z0AXYHI@TGTm3ex;=gxykUxr5V$G@01MzfEnIyL_66b*eI6#!mL#c--|UYI zLPXu!CwNDL7dvsuM5F!{PWS+O+2e{QUIklTnLBNAR$ZoK=qCAvr&c5;wnya8FkyPe zeU4T#o1NS%6F6&nR5P3(esgkduj9pF43G0o+iUUtrJQ~OO^cpP>F&&6qR;ZhPm>{! zHOtR7vJF&svf^V|yeQ$K{Vl;Wo~Fm2`*Ss$7dW?DJF`_eD}q$3@_q(`ZXgMLUd?j8 zxOZ-Kd(M7%ezJ5PnT?xyGOyNXmJwxPj&^}1Xg+InK|F|aiqao}67^M;%AiXxTOmqY z)0Rp%3sd?pSCJBdoS6HMC``jDQE=oHruH z)@G@{EYdD7EpN}OHZF>FEh(UJ*k>%E@XpCcal-$8XP{d))#h@vRfYYREV6~Ku^O%ct#K-pW3Yug zYbRH_Z5=6%mw(D**ZU?6Vhv7;tX6oe)Bfe}t@8Tz7h{Pl(tL7#;qUxR?%V?Evb4mg z19G+6#F{emAgbvmQtoEzpD5;V&-TgmIbZa(^F{ud6{FP+glCc>fjm_0Y%BQvcFu{? zbW{csTMP;}#-v!&ScCaP7Zy(T>XUNJ)nD-IdHag;03q@AS?vy+cl=hBHGw`F$%pyW zlwe3TW)sTV?zuPDlrEFqg2Ce?IC`^vF@Wu#^S&=C&c*thuNf>;7!n^As{ebwZH;En z&7&38WL39_h<}f~?1`1^BkSei3hrG<=WPW2#_>fHi*`8NZ_)5W=bZ}s13KU_Dzm)U z%O;AH-h{ACh1z_eWAZ-q=>i*${brrgXnkG{j$L86Q?s9%Y|LF(dw0 zFB=v+FcEp-7bt#oTq%%rv8}UetM)I9Y%?xjePB zJc6n^9GCie_=_AfT^*V8o#a6!)S;&gA*fz{JCK+6|!i)(#PJw?}!6VAeOtLvt)-AU!Uq<_b!*yZV_AHo(x-_ef30nnBw<~-;L3^sZT(8`-3K-FG*#j-$d;k%5{6> zC$$j8VVV|(^>dz()2-FzfmNN4zz`buQ-(*JKevtrx3HdBWUD_e?hnn?z^E9G!PY80{3ut$9SbDhl;um#*IaPh){ z`|^3a_(fizu&=U6by_#&0XXV^r|_Af0P=i!uaxAU!lU{(7NI(k1Juo!w0pV8^n0zR z5m6z0fyHjw`yL%#g8MF~c>({ZQyo83t|Q`qJiM3bqou`V&`xJbFFzHMdqX+Sqr2uV zbXpI4@tz>gR*R^kEt1COxqd+EK+0I&&&fWLNZKo>YP49oUz(JQ|;2F%mLnGnz6_(9v%ieKpriHZ4s_U@jjp z-q$bZz&u_)J~gpno98?_|4e*Xy~JY6kV#U^Q^l3A>sWAEQ@_X;OuL(4}aULWPy z%<&NQ;J()E`mN&P37-7){8`v@ja{|c$Es->`uDHSKM@R@6F~NrEN)njpY_oSF0K+C zd#K=}#6aKg?I@2Ro^%6(Xa*!W5f%bVuGxN{H=Khq{|+raWQ667e66DZ-%NjU7ZB}; zjTS1CRkoRElsoHP!1iJQRvANTZ2#5R-pgMcJmMbzs8h2SEse#nQ zzIR^SPsu$P^Es7^&S>B}S@2b$xB%V1HN(&QHedaH-|lSX2iH;?sW;ektuO4S7Um64 z@)F#ikLlvo6LUQ^=+bxPh)?`vrUge}3eVGN(?^T%!{pyizBRjog*LTdeP zbS$fSKZSDyu6U%==ZqnUwNI1>$tmdgoK#qBqiY0CZSt!;HL9)WQmN z?X30`2&^flfn$2cSHO`op+FU*%dFXx3WJVdt%#F$wrg4FPtg3(W#K~ zD__9QTjyIEAy5^;oCj;v)s*NPrMo#ZNZ0tN&wx@z&$og_7fSjiYZn270*R)juit$Y z7j+UQl}0;6vjl|9a_H`tsmG05NNwkbr?Y*Z94maKwTw+-iXeUg)4{)^_#zXRB?lN#r!l>O4k ze+t{;M=F}XRT%zw?|4jp@L?Y;s1rR;(XkIujF9Y|S76mq(fyYECJjH``c%i4x%gx1D z`UnPma}g(?aP$d}&r$ZBk$HHC3w9|Q+eh){{a6XPeTFZ)19rK*L=j&znSNg-DE*U# z6tY4T1#aap42u#9UuiWg8pbr|ZUPc3nYd5ij_chxF?O6W@i4iK8{n(E^m8)5rMnq7 zdD{>rYs1Ws>ONti=Bzwl$t>`FA>GEwnUT2D@O?kqV48ti=0Tza{*L36D>WAM%!dV1 z1f2FP-pvTeW%TL;=Y0Q`8 zp`hTRK%Z7+&d($lW04)+9*K$PM;B#-zrEj4unGNt`NBvf(*I9ih%Qsl^FMr{bRX^i z?h8w6K{_=((h_6mUO%^MJv#G^rY*9QC+s@5t~~hSstX8iI{l_E6_LNW1!`bO`-Q>6 z;eMcipqLLBpoqkz zLP4l_6MHbKHB5jlx6xV+%*iVxmcT9^h^jGPtX!*piU;KH8f#iPo7dtjz%AXVAP!g~ z2jWgp+04Hw?a=%LNgZo({Dvxt=;l#qbyDs(!UG|_>!~xZ!0b~H=>1i1Ig!k7KP0u? za9VC7n*+6OZw_=y`HJXScxtjoLp6!Pb?9lnmM?RCXe9)k+i%6nstedr{-OUp&c)_2 z)fKx2bS$H&sX!CKQS54Sws%*n^|-EMOEWhJuk*UV88OF1WD8%M9(+^l@dbbC5!ZeB zQ<96Y@STvAcSgoP0t6CVm(aTQBEhJbAW~Y}!Gvw@hW;Z=ySem04^lc9c9MXR{~48& z5j{pJX&w+8`Dz#Te9gN@SG7WFXo5!7v=>4sKx#mL5Aoarp;HO%`un{uhastW$rXJB z%L&m3Icc@Y0<^u30G|du%W_@4VP&#mrZQ;EqE^vzg)xfvaYmVV^`0>RSAEg~YP-CG-Ak67wYekM)GWJg<77;R? z-dt2Y*Uf9t^nhn#(maH>T17Dx@IvVu9xq~5T-%3K(mws$D+YFvu0h{&-A>2RHO<6m z)1AD+F;r);fOcj72Q&ChZ!i%BN}qmV%ayhBKS-VBb(@4E$%6hmX*70G8?WJ7^mQGf z7W|c5nngF+k%U7MDQ`T7>@22=*MG3a>3I8?N`mjl<5mQO=KT><;QC~w99x!Ca9V$8RbfbjC<8=iXD`v{l#sTfTlxuI zYMFV0C2&7GH0=4R3ZR~?5SZ3V5%6C*;Bf9j7(MvWfpH;kwlnuXXi6dOR;r-!662l>P(O#5L0z+xP-AR#$<)3Rg-6E4Imu?JV=L59Sk&fN2B z06DKmD~*5?x@@Kq#zLtWzWy9CYa%(Bv8psLwOwLPB9!cxfE>wJ-dMq)AMBrTWjnNQ zkQF!-N_#ZQ2Axx;j>%jeCKH`FavXUOk9o*NOnHk_Pld!swjo6>TBS{BNrq$b*ih`Bx6~6jJ8zn{R=DqudJJlaG zk1xqp{*{j$?s{LDMemWxy_Y=qe&K7m!8tp{Gh2J6-)#(tXywfzWU+a7zEV?oS5i^D zRa(K0&~DNi$zu#PS`9B!IWn77p?s-R?VEVo>#17R@=%FRer+rf2WcgFLoRo4$j7J* z(-(2Ak&|yMiQ?r8QtSc(2QlAs(V*s<-pf+_{2{-79m^IYF)BjNoj)5E-5nwR4oC5(?_Dl7kY;NQR+K z^VgB5X$=%f1h^zT-h(Awgu<(@l{P4Ts`JKc znXN7(wq)j1;?)afz$B#3TG>1q`2tOFrk5>r<{RF$74f`)K)G>`JLh=UJ_+CcB;A->DYZUzfg7=+V(W zHN%?d$GRjN@s>J78JzsWWNmpVy59WzAq0`k<>JLg*(>Yb()$H^g{Qqv5>d98@s0PY zl;jnS@JGVX3fAwpVg;xAEq_9_zn6e{vWSnHzf1}x+*`Ugo-~g>@j`71@)7ia^Jwp zgY==;k3G5Kbq=rhsI(svpMRoe{~=@c!Ei802l4+S0bkv(2V8rs`)8xP&3j$jGK5Z) zFfIJm>bic=^+s|L$U)f``9_NSV!!V9yyM$$g91nar{(~f-lJ~!{@5>*Y+x8PNBC_Z z{zc0D_*YTn|Cjg1_mxEB^(74V`p_RyNdy&f@dqz!plHcJQgx48pg47YSZLb0r~w7M zPy(&5Kg|QKpir_@RlrC9@HW7JDNu(ofYCzz**uUeAb<^~&iN=qEf|198ua=QNNO4s z73{{S6(kJ9<6ee-cnr!73gVCprVj`t^9Yt>#g`cIQ$|zfL-Uj*4WZ@@v1|#@5X4nL z^V4nh|C}1~gA}TT=13l~xJ_x8XHiHoD@8`BCZfC*-gg}?@CRPo`ms>!bE8%SZZMp-G$MK#PMZqrW(`E5 z^*wqYSwRX|d=Hpl1@yrpXGo*w1fv$Tq82@(mQ$lvTcg&Oqc$I-wn?LR1*7-1q7PcV zCs+wjTBFaFqc0w#uSjEV1Y_=wq9;9~!^Hjmw8p$H$NYPYxj_qi^Z-Aj#iDq|qNT;2 zvc`hgK*(jWxMXqoLNS;tv0PWN1Zi>PX>mlBacs~y3bJ^5&seJCI3_?mLs~q$b_|nc ze0gI$4_U(9ay)T({9DfiLHPtwc!KCk!l#{hWrKtdLW%Tc2?uhCa%2hOW#CU~i7I4q zGD6Xxg_7vo5|?F?5L#(*ZLLwtX-WE{F<&6j-=C69$YM1tL8fWR0MF!ixyg2;F*e~o z_ChH)q$yldDIUksZfqdWm6X%gl%9i>Ku9VNZEBEwDz`zZUwCSGS*qt~s@s052PBP? zHZ4Iujol#4B|I&yEX{s2jcq$E2a?`Emu@AWUSyeW8lKLan{GLp&a|Cg0m)#X%`lPA zXtK=E56__INp4R|=zNO*DU@XR6eZ4^`ME7=4VL+tEVFAgQ%*Z`2Q5qLIFn&JW1%f> zjxG8>5x6K1TwcjieahNq%jRgy`c0aB0Es=aj6P}0HjvMzf#eXZWP_fv?;tVv+6hmV zu`kE*k7+sB2+O4L<(x~8n454Q-T#J2H2*~^`;nFb|7WD~PKy$jf9bQu5cbJdj+6X< zMJiSC=ngA%e9=C+jQ`(=G(pc;ebmUE$Mhc}wfd;UP%S*(mp@6$-_4nasZr9!3!c$X1Q%GX!RDqk=h!y-ry zl*ca|`NX7-FcVFaEmGtL07ozZxkQShZ=AW2XrbZ(IpY5KwNfX=QuPu^ zX)V?0jxXeO|Oslr(d=VL?+X!F0xq2Sy9dBM&eHlQv z1=_?d$MXIQAjgdWWAfCZhDaG5sJK~fU}-20BeG`hNsZfMcSfjCPttCg3LCzwKpAVh zKBP`TfiWu!UmOd%Xr6N8D$iOkM}a4bmA$TP--fw!u(Th&$C2$1pI0$>Y9*-~Yj7vt z_mM3C&Gp;Av!z16KU=9_IjIhc16*G<^l@D>3O&DMukiJefBYA?c}IlzA0iRw>VJG@ z`y`I=$4n;P#sd+{Xk zEtJKoG#~S@&11{WcZ3xU_|A-UI*RLvbtgbf=W!Sykqu*vW7)T0^jc>-M)g;I+X_hJ z0PqwA)6MPd(IA_6(i`ff$F8Li3|TPI@TXfEr?WJUn7J8_@tV;vkEPLNv8=PvMY^7i zhUJ(86#~NoFIaaooB`n$K_jf~W(7xLWMO&EJVKU1y6o(MiK*-l7Vh+Z&n2b!u{z~; zp6$Jsl&0mYxy4fL&nH&FcIa%Bj;jh3)$dQ*E3AAbo>4sd-pWZ)E8X$j9WDpH4 z^<$-t9oMWCO~wY{Tt1cX9@}E0K#HNjy=Tp8uv>Noc}RD@ux#zQf=qVN4Lb=3=@@+0 zyn8o~&Fbg)aoGjQBR=MG2fb88t2N|j2ACOz=$pf`D#>}AKAmyKrE z)F7SRXaN?)v6DRp*@=+e{R_v$poGn*$@DBvnJGQ~2|f_Yiw)<@r!EN>LJ$p?cIk(d z9v7o7ziWO%nOh&eT>KwYUkB;lU`h|Ep=wzEVw~Y@4luFV2rjWyWaeL^FMPfK?3i&b zW#WozMzP7vIeGUJ=V@|aDxgLB-Ok6$?~60=H?!|1c&U89RY7y@bV`}Bd4DY4wQue`Nvs#aB8j8tXqx@Uz#S#cA1}BQ!qYvg2N_cxmy*FhK%*CY zSH7?K1OM*N{!~@ukGFE3uv&wtRT0x&)%r}n|FyNV3Z*PAMzl5aM63Qfo~?h zZ_&P=(zZ%GW!d@v1nnT-4g+vxX8esn26&tnSj2rb!T6apgcc*f9if;&K~epOu`n=o zH53|^ppE-wTcEDZA%L9|A49%%fHE5!CdFff&U3^=uMr16s`w9_at9FvFjbeYulW$6 zus-s{S+E{sDo_!vzhcw@#hwvqHPl@E-C~)yvz`RC?BFL9gV~r*XVe5JPgulOxN+=g z1!F@;ff7dc!Lh-;xHx1%5<|Q{^e0RpjAixcGS}d9=S?i>v_QIMM}`u92|7^Oknr2{ z=%h#{NGxgVhx+(%C}%!5B<&}%!%a$frU81|`{2*br*R{CNNg-Jql6aM@fMNAn0ikV zW^+Ahd*x>QB|>#}&6Q~0HXH0sZGm)iYGBcUoELq|*mTAaz~h9unpQw(%Op>7XJy74 zJbWn9{%tPWU4aTg0p2hEydplPO$zj~ihiqe!Vy4eTQzH_y!vXYWeiVIjZIJDMa( zY}CF?1R+7$7s^q-A@TSk$;OR+>$K=g7B{wlkfy54RypGV9iZQq$iJdW5k=}nS$~Ka zLk)Xt8x6eMxvC!v1296Xpjaomx=c%b_yz!BPXx^>QF-Z zrrhk&gIsWqt*@pXcAbW$_Z)==V5? zyIrUxE4mA?@9jGrD+*AI9oyl3z(P_5L>j&t6VE=5WPaD<(Kd*gPSgZ1{s93)l}rhr zA4c$1ZWwyTf$p6$^~})9g>1|pIO

      m|955OOey~({$bM3X+8~CD)$s@4CNuOs73$ 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 diff --git a/app/assets/images/spinning-circles.svg b/app/assets/images/spinning-circles.svg new file mode 100755 index 0000000000..258d405c49 --- /dev/null +++ b/app/assets/images/spinning-circles.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/javascripts/templates/registration/images/logo.html.haml b/app/assets/javascripts/templates/registration/images/logo.html.haml index 4b19c14cff..7dd08c90dc 100644 --- a/app/assets/javascripts/templates/registration/images/logo.html.haml +++ b/app/assets/javascripts/templates/registration/images/logo.html.haml @@ -40,6 +40,6 @@ .message{ ng: { hide: "imageSrc() || imageUploader.isUploading" } } Your logo will appear here for review once uploaded .loading{ ng: { hide: "!imageUploader.isUploading" } } - %img.spinner{ src: "/assets/loading.gif" } + %img.spinner{ src: "/assets/spinning-circles.svg" } %br/ Uploading... diff --git a/app/assets/javascripts/templates/registration/images/promo.html.haml b/app/assets/javascripts/templates/registration/images/promo.html.haml index d98721dc6d..6c3327c942 100644 --- a/app/assets/javascripts/templates/registration/images/promo.html.haml +++ b/app/assets/javascripts/templates/registration/images/promo.html.haml @@ -38,6 +38,6 @@ .message{ ng: { hide: "imageSrc() || imageUploader.isUploading" } } Your logo will appear here for review once uploaded .loading{ ng: { hide: "!imageUploader.isUploading" } } - %img.spinner{ src: "/assets/loading.gif" } + %img.spinner{ src: "/assets/spinning-circles.svg" } %br/ Uploading... diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index bf1b519fde..e247d7e3f2 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -26,10 +26,10 @@ %product{"ng-show" => "Products.loading"} .row.summary .small-12.columns.text-center - Loading products + Loading products... .row .small-12.columns.text-center - %img.spinner{ src: "/assets/loading.gif" } + %img.spinner{ src: "/assets/spinning-circles.svg" } %div{"ng-show" => "filteredProducts.length == 0 && !Products.loading"} .row.summary diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index f701813e9e..61bf9fbb7e 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -95,7 +95,7 @@ %span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "✓" || !column.visible && " " }} %span{ :class => 'two columns omega' } {{column.name }} %div.sixteen.columns.alpha#loading{ 'ng-if' => 'loading' } - %img.spinner{ src: "/assets/loading.gif" } + %img.spinner{ src: "/assets/spinning-circles.svg" } %h1 LOADING ORDERS %div{ :class => "sixteen columns alpha", 'ng-show' => '!loading && filteredLineItems.length == 0'} %h1#no_results No orders found. diff --git a/app/views/spree/admin/products/bulk_edit/_indicators.html.haml b/app/views/spree/admin/products/bulk_edit/_indicators.html.haml index d6ce039231..4760dacaeb 100644 --- a/app/views/spree/admin/products/bulk_edit/_indicators.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_indicators.html.haml @@ -2,7 +2,7 @@ {{ api_error_msg }} %div.sixteen.columns.alpha#loading{ 'ng-if' => 'loading' } - %img.spinner{ src: "/assets/loading.gif" } + %img.spinner{ src: "/assets/spinning-circles.svg" } %h1 LOADING PRODUCTS %div.sixteen.columns.alpha{ 'ng-show' => '!loading && filteredProducts.length == 0 && query.length==0' } From 41e42c78c428745e6c903dba7b15f53ed2643bff Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 16 Jan 2015 17:25:38 +1100 Subject: [PATCH 533/681] Update spec link name --- spec/features/consumer/shopping/variant_overrides_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index f7b1773e33..d7bd53784a 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -91,7 +91,7 @@ feature "shopping with variant overrides defined", js: true do fill_in "variants[#{v1.id}]", with: "2" show_cart wait_until_enabled 'li.cart a.button' - click_link 'Quick checkout' + click_link 'Checkout now' page.should have_selector 'form.edit_order .cart-total', text: '$122.21' page.should have_selector 'form.edit_order .shipping', text: '$0.00' @@ -105,7 +105,7 @@ feature "shopping with variant overrides defined", js: true do fill_in "variants[#{v1.id}]", with: "2" show_cart wait_until_enabled 'li.cart a.button' - click_link 'Quick checkout' + click_link 'Checkout now' complete_checkout @@ -124,7 +124,7 @@ feature "shopping with variant overrides defined", js: true do fill_in "variants[#{v4.id}]", with: "2" show_cart wait_until_enabled 'li.cart a.button' - click_link 'Quick checkout' + click_link 'Checkout now' expect do expect do @@ -137,7 +137,7 @@ feature "shopping with variant overrides defined", js: true do fill_in "variants[#{v1.id}]", with: "2" show_cart wait_until_enabled 'li.cart a.button' - click_link 'Quick checkout' + click_link 'Checkout now' expect do complete_checkout From 71de15b3e4514fd4ffbb19a1a672a75a8710aafe Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 21 Jan 2015 12:32:07 +1100 Subject: [PATCH 534/681] Contact address for enterprise group --- .../admin/enterprise_groups_controller.rb | 16 ++++++++ app/models/enterprise_group.rb | 9 +++++ .../admin/enterprise_groups/_inputs.html.haml | 37 +++++++++++++++++-- ..._add_addresses_ref_to_enterprise_groups.rb | 6 +++ db/schema.rb | 7 +++- 5 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb diff --git a/app/controllers/admin/enterprise_groups_controller.rb b/app/controllers/admin/enterprise_groups_controller.rb index cc2f3ed1db..7a0763f5a4 100644 --- a/app/controllers/admin/enterprise_groups_controller.rb +++ b/app/controllers/admin/enterprise_groups_controller.rb @@ -1,5 +1,7 @@ module Admin class EnterpriseGroupsController < ResourceController + before_filter :load_countries, :except => :index + def index end @@ -15,8 +17,22 @@ module Admin redirect_to main_app.admin_enterprise_groups_path end + protected + + def build_resource_with_address + enterprise_group = build_resource_without_address + enterprise_group.address = Spree::Address.new + enterprise_group.address.country = Spree::Country.find_by_id(Spree::Config[:default_country_id]) + enterprise_group + end + alias_method_chain :build_resource, :address + private + def load_countries + @countries = Spree::Country.order(:name) + end + def collection EnterpriseGroup.by_position end diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 6e61b1a564..25fcadca5d 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -2,6 +2,10 @@ class EnterpriseGroup < ActiveRecord::Base acts_as_list has_and_belongs_to_many :enterprises + belongs_to :address, :class_name => 'Spree::Address' + accepts_nested_attributes_for :address + validates :address, presence: true, associated: true + before_validation :set_unused_address_fields validates :name, presence: true validates :description, presence: true @@ -28,4 +32,9 @@ class EnterpriseGroup < ActiveRecord::Base scope :by_position, order('position ASC') scope :on_front_page, where(on_front_page: true) + + def set_unused_address_fields + address.firstname = address.lastname = address.phone = 'unused' if address.present? + end + end diff --git a/app/views/admin/enterprise_groups/_inputs.html.haml b/app/views/admin/enterprise_groups/_inputs.html.haml index d3baec4a0c..82fdc3eece 100644 --- a/app/views/admin/enterprise_groups/_inputs.html.haml +++ b/app/views/admin/enterprise_groups/_inputs.html.haml @@ -47,6 +47,37 @@ = image_tag @object.promo_image.url if @object.promo_image.present? = f.file_field :promo_image -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } - %legend Contact - TODO += f.fields_for :address do |af| + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } + %legend Contact + .row + .three.columns.alpha + = af.label :address1 + %span.required * + .eight.columns.omega + = af.text_field :address1, { placeholder: "eg. 123 High Street"} + .row + .alpha.three.columns + = af.label :address2 + .eight.columns.omega + = af.text_field :address2 + .row + .three.columns.alpha + = af.label :city, 'Suburb' + \/ + = af.label :zipcode, 'Postcode' + %span.required * + .four.columns + = af.text_field :city, { placeholder: "eg. Northcote"} + .four.columns.omega + = af.text_field :zipcode, { placeholder: "eg. 3070"} + .row + .three.columns.alpha + = af.label :state_id, 'State' + \/ + = af.label :country_id, 'Country' + %span.required * + .four.columns + = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" + .four.columns.omega + = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" diff --git a/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb b/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb new file mode 100644 index 0000000000..8b2ca415b1 --- /dev/null +++ b/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb @@ -0,0 +1,6 @@ +class AddAddressesRefToEnterpriseGroups < ActiveRecord::Migration + def change + add_column :enterprise_groups, :address_id, :integer + add_foreign_key :enterprise_groups, :spree_addresses, name: "enterprise_groups_address_id_fk", column: "address_id" + end +end diff --git a/db/schema.rb b/db/schema.rb index 3244916b3b..16c616f1a3 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 => 20141210233407) do +ActiveRecord::Schema.define(:version => 20150115050935) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -197,6 +197,7 @@ ActiveRecord::Schema.define(:version => 20141210233407) do t.string "logo_content_type" t.integer "logo_file_size" t.datetime "logo_updated_at" + t.integer "address_id" end create_table "enterprise_groups_enterprises", :id => false, :force => true do |t| @@ -573,9 +574,9 @@ ActiveRecord::Schema.define(:version => 20141210233407) 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 @@ -1077,6 +1078,8 @@ ActiveRecord::Schema.define(:version => 20141210233407) do add_foreign_key "enterprise_fees", "enterprises", name: "enterprise_fees_enterprise_id_fk" + add_foreign_key "enterprise_groups", "spree_addresses", name: "enterprise_groups_address_id_fk", column: "address_id" + add_foreign_key "enterprise_groups_enterprises", "enterprise_groups", name: "enterprise_groups_enterprises_enterprise_group_id_fk" add_foreign_key "enterprise_groups_enterprises", "enterprises", name: "enterprise_groups_enterprises_enterprise_id_fk" From 650e35c13e8497910b5618a118e711fc1dd24f5b Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 21 Jan 2015 15:34:31 +1100 Subject: [PATCH 535/681] Adding contact information to groups backend --- .../side_menu_controller.js.coffee | 1 + app/models/enterprise_group.rb | 3 ++ .../admin/enterprise_groups/_inputs.html.haml | 38 +++++++++++++++++++ ...627_add_web_conact_to_enterprise_groups.rb | 10 +++++ db/schema.rb | 8 +++- 5 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20150121030627_add_web_conact_to_enterprise_groups.rb diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee index 652f06e426..63f05a9376 100644 --- a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee @@ -8,6 +8,7 @@ angular.module("admin.enterprise_groups") { name: 'About', icon_class: "icon-pencil" } { name: 'Images', icon_class: "icon-picture" } { name: 'Contact', icon_class: "icon-phone" } + { name: 'Web', icon_class: "icon-globe" } ] $scope.select(0) diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 25fcadca5d..5b3b65e311 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -12,6 +12,9 @@ class EnterpriseGroup < ActiveRecord::Base attr_accessible :name, :description, :long_description, :on_front_page, :enterprise_ids attr_accessible :logo, :promo_image + attr_accessible :email, :website, :facebook, :instagram, :linkedin, :twitter + + delegate :phone, :to => :address has_attached_file :logo, styles: {medium: "100x100"}, diff --git a/app/views/admin/enterprise_groups/_inputs.html.haml b/app/views/admin/enterprise_groups/_inputs.html.haml index 82fdc3eece..fabebdaef4 100644 --- a/app/views/admin/enterprise_groups/_inputs.html.haml +++ b/app/views/admin/enterprise_groups/_inputs.html.haml @@ -50,6 +50,11 @@ = f.fields_for :address do |af| %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } %legend Contact + .row + .alpha.three.columns + = f.label :phone + .omega.eight.columns + = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} .row .three.columns.alpha = af.label :address1 @@ -81,3 +86,36 @@ = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" .four.columns.omega = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" + +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Web'" } } + %legend Web Resources + .row + .alpha.three.columns + = f.label :website + .omega.eight.columns + = f.text_field :website, { placeholder: "eg. www.truffles.com"} + .row + .alpha.three.columns + = f.label :email + .omega.eight.columns + = f.text_field :email + .row + .alpha.three.columns + = f.label :facebook, 'Facebook' + .omega.eight.columns + = f.text_field :facebook + .row + .alpha.three.columns + = f.label :instagram, 'Instagram' + .omega.eight.columns + = f.text_field :instagram + .row + .alpha.three.columns + = f.label :linkedin, 'LinkedIn' + .omega.eight.columns + = f.text_field :linkedin + .row + .alpha.three.columns + = f.label :twitter + .omega.eight.columns + = f.text_field :twitter, { placeholder: "eg. @the_prof" } diff --git a/db/migrate/20150121030627_add_web_conact_to_enterprise_groups.rb b/db/migrate/20150121030627_add_web_conact_to_enterprise_groups.rb new file mode 100644 index 0000000000..1aca6a0e16 --- /dev/null +++ b/db/migrate/20150121030627_add_web_conact_to_enterprise_groups.rb @@ -0,0 +1,10 @@ +class AddWebConactToEnterpriseGroups < ActiveRecord::Migration + def change + add_column :enterprise_groups, :email, :string + add_column :enterprise_groups, :website, :string + add_column :enterprise_groups, :facebook, :string + add_column :enterprise_groups, :instagram, :string + add_column :enterprise_groups, :linkedin, :string + add_column :enterprise_groups, :twitter, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 16c616f1a3..418f5a7943 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 => 20150115050935) do +ActiveRecord::Schema.define(:version => 20150121030627) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -198,6 +198,12 @@ ActiveRecord::Schema.define(:version => 20150115050935) do t.integer "logo_file_size" t.datetime "logo_updated_at" t.integer "address_id" + t.string "email" + t.string "website" + t.string "facebook" + t.string "instagram" + t.string "linkedin" + t.string "twitter" end create_table "enterprise_groups_enterprises", :id => false, :force => true do |t| From c1aa2f9b3367863b697920135e305fdb940f0c12 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 21 Jan 2015 16:12:28 +1100 Subject: [PATCH 536/681] Display contact information in the front end --- app/views/groups/show.html.haml | 64 +++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 2ec509c0c5..cd50aa90a9 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -79,8 +79,68 @@ %tab{heading: 'Contact us', active: "active(\'contact\')", select: "select(\'contact\')"} - %h3 Contact us - %p TODO: provide contact information + .row + .small-6.columns + %h3 Contact us + - if @group.phone + .row + .small-2.columns + Call + .small-10.columns + = @group.phone + - if @group.email + .row + .small-2.columns + Email + .small-10.columns + = @group.email + - if @group.website + .row + .small-2.columns + Website + .small-10.columns + = @group.website + .small-6.columns + %h3 Follow us + - if @group.facebook + .row + .small-2.columns + Facebook + .small-10.columns + = @group.facebook + - if @group.instagram + .row + .small-2.columns + Instagram + .small-10.columns + = @group.instagram + - if @group.linkedin + .row + .small-2.columns + LinkedIn + .small-10.columns + = @group.linkedin + - if @group.twitter + .row + .small-2.columns + Twitter + .small-10.columns + = @group.twitter + .row + .small-6.columns + %h3 Address + %p + = @group.address.address1 + - if @group.address.address2 + %br + = @group.address.address2 + %br + = @group.address.city + , + = @group.address.state + = @group.address.zipcode + %br + = @group.address.country %p TODO: show the group's contact footer From 0d9a0919e51eba086a5ff41c0146d596fc764ab3 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 21 Jan 2015 16:39:41 +1100 Subject: [PATCH 537/681] show contact a group's contact footer --- app/views/groups/show.html.haml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index cd50aa90a9..c354dd36a9 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -142,6 +142,18 @@ %br = @group.address.country - %p TODO: show the group's contact footer + .text-center + %p + = @group.name + %p + -if @group.facebook + %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} + %i.ofn-i_044-facebook + -if @group.email + %a{title:'Email us', href: @group.email.reverse, mailto: true} + %i.ofn-i_050-mail-circle + -if @group.website + %a{title:'Visit our website', href: 'http://' + @group.website, target: '_blank'} + %i.ofn-i_049-web = render partial: "shared/footer" From 9f0aeb5adfc15ea0ef895bab02f40d4bf07f88ed Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 19 Dec 2014 16:07:51 +1100 Subject: [PATCH 538/681] Adding unique permalink to enterprises --- app/models/enterprise.rb | 3 ++- ...1219034321_add_permalink_to_enterprises.rb | 22 +++++++++++++++++++ db/schema.rb | 3 ++- spec/factories.rb | 1 + spec/models/enterprise_spec.rb | 4 +++- 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20141219034321_add_permalink_to_enterprises.rb diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 929fcd1a7e..9c516666a7 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -56,6 +56,7 @@ class Enterprise < ActiveRecord::Base validates :address, presence: true, associated: true validates :email, presence: true validates_presence_of :owner + validates :permalink, uniqueness: true, presence: true validate :shopfront_taxons validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? } validates_length_of :description, :maximum => 255 @@ -332,7 +333,7 @@ class Enterprise < ActiveRecord::Base end def geocode_address - address.geocode if address.changed? + address.geocode if address.andand.changed? end def ensure_owner_is_manager diff --git a/db/migrate/20141219034321_add_permalink_to_enterprises.rb b/db/migrate/20141219034321_add_permalink_to_enterprises.rb new file mode 100644 index 0000000000..d233a18d5d --- /dev/null +++ b/db/migrate/20141219034321_add_permalink_to_enterprises.rb @@ -0,0 +1,22 @@ +class AddPermalinkToEnterprises < ActiveRecord::Migration + def up + add_column :enterprises, :permalink, :string + + Enterprise.all.each do |enterprise| + counter = 1 + permalink = enterprise.name.parameterize + while Enterprise.find_by_permalink(permalink) do + permalink = enterprise.name.parameterize + counter.to_s + counter += 1 + end + + enterprise.update_attributes!(permalink: permalink) + end + + change_column :enterprises, :permalink, :string, null: false + end + + def down + add_column :enterprises, :permalink + end +end diff --git a/db/schema.rb b/db/schema.rb index 3244916b3b..2d8f28fbdd 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 => 20141210233407) do +ActiveRecord::Schema.define(:version => 20141219034321) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -271,6 +271,7 @@ ActiveRecord::Schema.define(:version => 20141210233407) do t.string "unconfirmed_email" t.datetime "shop_trial_start_date" t.boolean "producer_profile_only", :default => false + t.string "permalink", :null => false end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" diff --git a/spec/factories.rb b/spec/factories.rb index 214c61962d..33d760ec15 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -97,6 +97,7 @@ FactoryGirl.define do factory :enterprise, :class => Enterprise do owner { FactoryGirl.create :user } sequence(:name) { |n| "Enterprise #{n}" } + sequence(:permalink) { |n| "enterprise#{n}" } sells 'any' description 'enterprise' long_description '

      Hello, world!

      This is a paragraph.

      ' diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 37535a2c3c..9edc1d2cee 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -165,9 +165,11 @@ describe Enterprise do end describe "validations" do - subject { FactoryGirl.create(:distributor_enterprise, :address => FactoryGirl.create(:address)) } + subject { FactoryGirl.create(:distributor_enterprise) } it { should validate_presence_of(:name) } it { should validate_presence_of(:email) } + it { should validate_presence_of(:permalink) } + it { should validate_uniqueness_of(:permalink) } it { should ensure_length_of(:description).is_at_most(255) } it "requires an owner" do From e2268e53bb715085a97b8fb5959a8958ca1bf885 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 14 Jan 2015 12:16:24 +1100 Subject: [PATCH 539/681] Adding action to check uniquness of enterprise permalink against existing routes --- .../admin/enterprises_controller.rb | 14 +++++++++++- config/routes.rb | 1 + .../admin/enterprises_controller_spec.rb | 22 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index cf97b5a283..53ef01207c 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -1,7 +1,7 @@ module Admin class EnterprisesController < ResourceController before_filter :load_enterprise_set, :only => :index - before_filter :load_countries, :except => :index + before_filter :load_countries, :except => [:index, :set_sells, :check_permalink] before_filter :load_methods_and_fees, :only => [:new, :edit, :update, :create] before_filter :load_taxons, :only => [:new, :edit, :update, :create] before_filter :check_can_change_sells, only: :update @@ -53,6 +53,18 @@ module Admin end end + def check_permalink + path = Rails.application.routes.recognize_path( "/#{ params[:permalink].to_s }" ) + if path && path[:controller] == "cms_content" + respond_to do |format| + format.js { render nothing: true, status: 200 } + end + else + respond_to do |format| + format.js { render nothing: true, status: 409 } + end + end + end protected diff --git a/config/routes.rb b/config/routes.rb index 37c1974875..35f7154358 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,7 @@ Openfoodnetwork::Application.routes.draw do collection do get :for_order_cycle post :bulk_update, as: :bulk_update + get :check_permalink end member do diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index cda73d4079..32281db681 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -282,5 +282,27 @@ module Admin end end end + + describe "checking permalink suitability" do + # let(:enterprise) { create(:enterprise, permalink: 'enterprise_permalink') } + + before do + controller.stub spree_current_user: admin_user + end + + it "responds with status of 200 when the route does not exist" do + spree_get :check_permalink, { permalink: 'some_nonexistent_route', format: :js } + expect(response.status).to be 200 + end + + it "responds with status of 409 when the permalink matches an existing route" do + # spree_get :check_permalink, { permalink: 'enterprise_permalink', format: :js } + # expect(response.status).to be 409 + spree_get :check_permalink, { permalink: 'map', format: :js } + expect(response.status).to be 409 + spree_get :check_permalink, { permalink: '', format: :js } + expect(response.status).to be 409 + end + end end end From c330e49a7fb4571c2b08ce15cbd488917eb0d0c8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 14 Jan 2015 14:26:21 +1100 Subject: [PATCH 540/681] Moving permalink check action to enterprise controller --- .../admin/enterprises_controller.rb | 13 ----------- app/controllers/enterprises_controller.rb | 13 +++++++++++ config/routes.rb | 2 +- .../admin/enterprises_controller_spec.rb | 22 ------------------- .../enterprises_controller_spec.rb | 18 +++++++++++++++ 5 files changed, 32 insertions(+), 36 deletions(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 53ef01207c..af2f2f5425 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -53,19 +53,6 @@ module Admin end end - def check_permalink - path = Rails.application.routes.recognize_path( "/#{ params[:permalink].to_s }" ) - if path && path[:controller] == "cms_content" - respond_to do |format| - format.js { render nothing: true, status: 200 } - end - else - respond_to do |format| - format.js { render nothing: true, status: 409 } - end - end - end - protected def build_resource_with_address diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 298c1d5a10..571d17e3eb 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -69,4 +69,17 @@ class EnterprisesController < BaseController redirect_to main_app.shop_path end + + def check_permalink + path = Rails.application.routes.recognize_path( "/#{ params[:permalink].to_s }" ) + if path && path[:controller] == "cms_content" + respond_to do |format| + format.js { render nothing: true, status: 200 } + end + else + respond_to do |format| + format.js { render nothing: true, status: 409 } + end + end + end end diff --git a/config/routes.rb b/config/routes.rb index 35f7154358..d1cb9c93ae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,6 +26,7 @@ Openfoodnetwork::Application.routes.draw do get :suppliers get :distributors post :search + get :check_permalink end member do @@ -46,7 +47,6 @@ Openfoodnetwork::Application.routes.draw do collection do get :for_order_cycle post :bulk_update, as: :bulk_update - get :check_permalink end member do diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 32281db681..cda73d4079 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -282,27 +282,5 @@ module Admin end end end - - describe "checking permalink suitability" do - # let(:enterprise) { create(:enterprise, permalink: 'enterprise_permalink') } - - before do - controller.stub spree_current_user: admin_user - end - - it "responds with status of 200 when the route does not exist" do - spree_get :check_permalink, { permalink: 'some_nonexistent_route', format: :js } - expect(response.status).to be 200 - end - - it "responds with status of 409 when the permalink matches an existing route" do - # spree_get :check_permalink, { permalink: 'enterprise_permalink', format: :js } - # expect(response.status).to be 409 - spree_get :check_permalink, { permalink: 'map', format: :js } - expect(response.status).to be 409 - spree_get :check_permalink, { permalink: '', format: :js } - expect(response.status).to be 409 - end - end end end diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index fa8ca888e8..34d095c29a 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -109,4 +109,22 @@ describe EnterprisesController do response.should redirect_to spree.root_path end end + + context "checking permalink availability" do + # let(:enterprise) { create(:enterprise, permalink: 'enterprise_permalink') } + + it "responds with status of 200 when the route does not exist" do + spree_get :check_permalink, { permalink: 'some_nonexistent_route', format: :js } + expect(response.status).to be 200 + end + + it "responds with status of 409 when the permalink matches an existing route" do + # spree_get :check_permalink, { permalink: 'enterprise_permalink', format: :js } + # expect(response.status).to be 409 + spree_get :check_permalink, { permalink: 'map', format: :js } + expect(response.status).to be 409 + spree_get :check_permalink, { permalink: '', format: :js } + expect(response.status).to be 409 + end + end end From 9286c82b43cf46c23d66c17092bea8c1c73e0daf Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 15 Jan 2015 15:08:46 +1100 Subject: [PATCH 541/681] Removing obsolete methods --- .../spree/orders_controller_decorator.rb | 19 -------------- config/routes.rb | 2 -- .../spree/orders_controller_spec.rb | 26 ------------------- 3 files changed, 47 deletions(-) diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index a1b468ae2d..fd5d3b634e 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -33,25 +33,6 @@ Spree::OrdersController.class_eval do end end - def select_distributor - distributor = Enterprise.is_distributor.find params[:id] - - order = current_order(true) - order.distributor = distributor - order.save! - - redirect_to main_app.enterprise_path(distributor) - end - - def deselect_distributor - order = current_order(true) - - order.distributor = nil - order.save! - - redirect_to root_path - end - def update_distribution @order = current_order(true) diff --git a/config/routes.rb b/config/routes.rb index d1cb9c93ae..912741322e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -163,8 +163,6 @@ Spree::Core::Engine.routes.prepend do end resources :orders do - get :select_distributor, :on => :member - get :deselect_distributor, :on => :collection get :clear, :on => :collection get :order_cycle_expired, :on => :collection end diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 1d2f41f86c..52bd21b500 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -40,32 +40,6 @@ describe Spree::OrdersController do flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." end - it "selects distributors" do - d = create(:distributor_enterprise) - p = create(:product, :distributors => [d]) - - spree_get :select_distributor, :id => d.id - response.should be_redirect - - order = subject.current_order(false) - order.distributor.should == d - end - - it "deselects distributors" do - d = create(:distributor_enterprise) - p = create(:product, :distributors => [d]) - - order = subject.current_order(true) - order.distributor = d - order.save! - - spree_get :deselect_distributor - response.should be_redirect - - order.reload - order.distributor.should be_nil - end - context "adding a group buy product to the cart" do it "sets a variant attribute for the max quantity" do distributor_product = create(:distributor_enterprise) From 7a6cd98646d941d86a55b27d3875e66140dbc72e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 15 Jan 2015 16:33:04 +1100 Subject: [PATCH 542/681] Fixing failing spec, missing permalink --- spec/controllers/admin/enterprises_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index cda73d4079..01bfebfa42 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -24,7 +24,7 @@ module Admin describe "creating an enterprise" do let(:country) { Spree::Country.find_by_name 'Australia' } let(:state) { Spree::State.find_by_name 'Victoria' } - let(:enterprise_params) { {enterprise: {name: 'zzz', email: "bob@example.com", address_attributes: {address1: 'a', city: 'a', zipcode: 'a', country_id: country.id, state_id: state.id}}} } + let(:enterprise_params) { {enterprise: {name: 'zzz', permalink: 'zzz', email: "bob@example.com", address_attributes: {address1: 'a', city: 'a', zipcode: 'a', country_id: country.id, state_id: state.id}}} } it "grants management permission if the current user is an enterprise user" do controller.stub spree_current_user: user From c3659612ed12df092d8256cf25aa601319c1cf9c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 16 Jan 2015 10:12:17 +1100 Subject: [PATCH 543/681] enterprise routes use permalink --- .../admin/enterprises_controller.rb | 7 ++++++- app/controllers/api/enterprises_controller.rb | 4 ++-- app/controllers/enterprises_controller.rb | 4 ++-- app/models/enterprise.rb | 2 +- .../admin/enterprises_controller_spec.rb | 20 +++++++++---------- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index af2f2f5425..9232144c21 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -18,7 +18,7 @@ module Admin end def set_sells - enterprise = Enterprise.find(params[:id]) + enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id]) attributes = { sells: params[:sells] } attributes[:producer_profile_only] = params[:sells] == "none" && !!params[:producer_profile_only] attributes[:shop_trial_start_date] = Time.now if params[:sells] == "own" @@ -63,6 +63,11 @@ module Admin end alias_method_chain :build_resource, :address + # Overriding method on Spree's resource controller, + # so that resources are found using permalink + def find_resource + Enterprise.find_by_permalink(params[:id]) + end private diff --git a/app/controllers/api/enterprises_controller.rb b/app/controllers/api/enterprises_controller.rb index cba50c4923..d40d67409d 100644 --- a/app/controllers/api/enterprises_controller.rb +++ b/app/controllers/api/enterprises_controller.rb @@ -29,7 +29,7 @@ module Api end def update - @enterprise = Enterprise.find(params[:id]) + @enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id]) authorize! :update, @enterprise if @enterprise.update_attributes(params[:enterprise]) @@ -40,7 +40,7 @@ module Api end def update_image - @enterprise = Enterprise.find(params[:id]) + @enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id]) authorize! :update, @enterprise if params[:logo] && @enterprise.update_attributes( { logo: params[:logo] } ) diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 571d17e3eb..27a335d9c4 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -24,7 +24,7 @@ class EnterprisesController < BaseController end def show - @enterprise = Enterprise.find params[:id] + @enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id]) # User can view this page if they've already chosen their distributor, or if this page # is for a supplier, they may use it to select a distributor that sells this supplier's @@ -53,7 +53,7 @@ class EnterprisesController < BaseController end def shop - distributor = Enterprise.is_distributor.find params[:id] + distributor = Enterprise.is_distributor.find_by_permalink(params[:id]) || Enterprise.is_distributor.find(params[:id]) order = current_order(true) if order.distributor and order.distributor != distributor diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 9c516666a7..fbb525cfe8 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -202,7 +202,7 @@ class Enterprise < ActiveRecord::Base end def to_param - "#{id}-#{name.parameterize}" + permalink end def relatives diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 01bfebfa42..6b29d00107 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -90,7 +90,7 @@ module Admin it "does not allow 'sells' to be changed" do profile_enterprise.enterprise_roles.build(user: user).save controller.stub spree_current_user: user - enterprise_params = { id: profile_enterprise.id, enterprise: { sells: 'any' } } + enterprise_params = { id: profile_enterprise, enterprise: { sells: 'any' } } spree_put :update, enterprise_params profile_enterprise.reload @@ -101,7 +101,7 @@ module Admin context "as super admin" do it "allows 'sells' to be changed" do controller.stub spree_current_user: admin_user - enterprise_params = { id: profile_enterprise.id, enterprise: { sells: 'any' } } + enterprise_params = { id: profile_enterprise, enterprise: { sells: 'any' } } spree_put :update, enterprise_params profile_enterprise.reload @@ -131,7 +131,7 @@ module Admin context "allows setting 'sells' to 'none'" do it "is allowed" do - spree_post :set_sells, { id: enterprise.id, sells: 'none' } + spree_post :set_sells, { id: enterprise, sells: 'none' } expect(response).to redirect_to spree.admin_path expect(flash[:success]).to eq "Congratulations! Registration for #{enterprise.name} is complete!" expect(enterprise.reload.sells).to eq 'none' @@ -139,7 +139,7 @@ module Admin context "setting producer_profile_only to true" do it "is allowed" do - spree_post :set_sells, { id: enterprise.id, sells: 'none', producer_profile_only: true } + spree_post :set_sells, { id: enterprise, sells: 'none', producer_profile_only: true } expect(response).to redirect_to spree.admin_path expect(enterprise.reload.producer_profile_only).to eq true end @@ -159,7 +159,7 @@ module Admin end it "is disallowed" do - spree_post :set_sells, { id: enterprise.id, sells: 'own' } + spree_post :set_sells, { id: enterprise, sells: 'own' } expect(response).to redirect_to spree.admin_path trial_expiry = Date.today.strftime("%Y-%m-%d") expect(flash[:error]).to eq "Sorry, but you've already had a trial. Expired on: #{trial_expiry}" @@ -175,7 +175,7 @@ module Admin end it "is allowed, but trial start date is not reset" do - spree_post :set_sells, { id: enterprise.id, sells: 'own' } + spree_post :set_sells, { id: enterprise, sells: 'own' } expect(response).to redirect_to spree.admin_path trial_expiry = (Date.today + 30.days).strftime("%Y-%m-%d") expect(flash[:notice]).to eq "Welcome back! Your trial expires on: #{trial_expiry}" @@ -186,7 +186,7 @@ module Admin context "if a trial has not started" do it "is allowed" do - spree_post :set_sells, { id: enterprise.id, sells: 'own' } + spree_post :set_sells, { id: enterprise, sells: 'own' } expect(response).to redirect_to spree.admin_path expect(flash[:success]).to eq "Congratulations! Registration for #{enterprise.name} is complete!" expect(enterprise.reload.sells).to eq 'own' @@ -196,7 +196,7 @@ module Admin context "setting producer_profile_only to true" do it "is ignored" do - spree_post :set_sells, { id: enterprise.id, sells: 'own', producer_profile_only: true } + spree_post :set_sells, { id: enterprise, sells: 'own', producer_profile_only: true } expect(response).to redirect_to spree.admin_path expect(enterprise.reload.producer_profile_only).to be false end @@ -205,7 +205,7 @@ module Admin context "setting 'sells' to any" do it "is not allowed" do - spree_post :set_sells, { id: enterprise.id, sells: 'any' } + spree_post :set_sells, { id: enterprise, sells: 'any' } expect(response).to redirect_to spree.admin_path expect(flash[:error]).to eq "Unauthorised" expect(enterprise.reload.sells).to eq 'none' @@ -214,7 +214,7 @@ module Admin context "settiing 'sells' to 'unspecified'" do it "is not allowed" do - spree_post :set_sells, { id: enterprise.id, sells: 'unspecified' } + spree_post :set_sells, { id: enterprise, sells: 'unspecified' } expect(response).to redirect_to spree.admin_path expect(flash[:error]).to eq "Unauthorised" expect(enterprise.reload.sells).to eq 'none' From 39889390ef9bc753441a854677fca59703b9e8c8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 16 Jan 2015 12:35:58 +1100 Subject: [PATCH 544/681] Altering shop page routing so that shop url is copy-and-pastable --- app/controllers/enterprises_controller.rb | 16 ++++++++++++++-- app/controllers/shop_controller.rb | 12 +----------- .../shop.html.haml} | 0 config/routes.rb | 1 + 4 files changed, 16 insertions(+), 13 deletions(-) rename app/views/{shop/show.html.haml => enterprises/shop.html.haml} (100%) diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 27a335d9c4..4414dd4ece 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -1,6 +1,9 @@ class EnterprisesController < BaseController + layout "darkswarm" helper Spree::ProductsHelper include OrderCyclesHelper + before_filter :set_order_cycles, only: :shop + before_filter :load_active_distributors, only: :shop def index @enterprises = Enterprise.all @@ -66,8 +69,6 @@ class EnterprisesController < BaseController order_cycle_options = OrderCycle.active.with_distributor(distributor) order.order_cycle = order_cycle_options.first if order_cycle_options.count == 1 order.save! - - redirect_to main_app.shop_path end def check_permalink @@ -82,4 +83,15 @@ class EnterprisesController < BaseController end end end + + private + + def set_order_cycles + @order_cycles = OrderCycle.with_distributor(@distributor).active + + # And default to the only order cycle if there's only the one + if @order_cycles.count == 1 + current_order(true).set_order_cycle! @order_cycles.first + end + end end diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index c0c2c225a5..c350791158 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -3,10 +3,9 @@ require 'open_food_network/scope_product_to_hub' class ShopController < BaseController layout "darkswarm" before_filter :require_distributor_chosen - before_filter :set_order_cycles - before_filter :load_active_distributors def show + redirect_to main_app.enterprise_shop_path(current_distributor) end def products @@ -36,15 +35,6 @@ class ShopController < BaseController private - def set_order_cycles - @order_cycles = OrderCycle.with_distributor(@distributor).active - - # And default to the only order cycle if there's only the one - if @order_cycles.count == 1 - current_order(true).set_order_cycle! @order_cycles.first - end - end - def products_for_shop if current_order_cycle current_order_cycle. diff --git a/app/views/shop/show.html.haml b/app/views/enterprises/shop.html.haml similarity index 100% rename from app/views/shop/show.html.haml rename to app/views/enterprises/shop.html.haml diff --git a/config/routes.rb b/config/routes.rb index 912741322e..0d5d034df4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,6 +34,7 @@ Openfoodnetwork::Application.routes.draw do get :shop # old world end end + get '/:id/shop', to: 'enterprises#shop', as: 'enterprise_shop' devise_for :enterprise, controllers: { confirmations: 'enterprise_confirmations' } From 3ec3441cfa0f877b1050c447f0eb66c92a6cedbe Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 16 Jan 2015 12:57:48 +1100 Subject: [PATCH 545/681] Replacing all uses of the old enterprise shop url helper --- .../_header/replace_logo_with_link_text.html.haml.deface | 2 +- app/serializers/api/enterprise_serializer.rb | 4 +--- .../admin/enterprises/form/_primary_details.html.haml | 2 +- app/views/enterprise_mailer/welcome.html.haml | 4 ++-- app/views/enterprises/show.html.haml | 2 +- app/views/json/partials/_hub.rabl | 2 +- app/views/shared/_account_sidebar.html.haml | 8 ++++---- app/views/shared/_distributor.html.haml | 2 +- 8 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/overrides/spree/shared/_header/replace_logo_with_link_text.html.haml.deface b/app/overrides/spree/shared/_header/replace_logo_with_link_text.html.haml.deface index 5d090321f0..a8571d30bb 100644 --- a/app/overrides/spree/shared/_header/replace_logo_with_link_text.html.haml.deface +++ b/app/overrides/spree/shared/_header/replace_logo_with_link_text.html.haml.deface @@ -2,7 +2,7 @@ %figure#logo.columns.eight - if current_distributor - %h1= link_to current_distributor.name, main_app.shop_enterprise_path(current_distributor) + %h1= link_to current_distributor.name, main_app.enterprise_shop_path(current_distributor) .change-location= link_to 'Change Location', root_path - else %h1= link_to "OPEN FOOD NETWORK", root_path diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 3e6708b526..95ef4cec60 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -69,10 +69,8 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer object.promo_image(:large) if object.promo_image.exists? end - # TODO when ActiveSerializers supports URL helpers - # Then refactor. See readme https://github.com/rails-api/active_model_serializers def path - "/enterprises/#{object.to_param}/shop" + enterprise_shop_path(object) end # Map svg icons. diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml index e25060fb14..e42e7038e2 100644 --- a/app/views/admin/enterprises/form/_primary_details.html.haml +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -72,4 +72,4 @@ .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} %a What's this? .eight.columns.omega - = main_app.shop_enterprise_url(@enterprise) \ No newline at end of file + = main_app.enterprise_shop_url(@enterprise) \ No newline at end of file diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index 74840fbefa..5c69caae77 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -23,8 +23,8 @@ -# Shop URL -# %td   -# %td --# %a{:href => "#{ main_app.shop_enterprise_url(@enterprise) }", :target => "_blank"} --# = main_app.shop_enterprise_url(@enterprise) +-# %a{:href => "#{ main_app.enterprise_shop_url(@enterprise) }", :target => "_blank"} +-# = main_app.enterprise_shop_url(@enterprise) -# %tr -# %td   -# %tr diff --git a/app/views/enterprises/show.html.haml b/app/views/enterprises/show.html.haml index 3c672b4cdd..8e5035dbcc 100644 --- a/app/views/enterprises/show.html.haml +++ b/app/views/enterprises/show.html.haml @@ -12,7 +12,7 @@ %ul#supplier-distributors - if @distributors.delete @enterprise - %li= link_to "Buy direct from the farm", shop_enterprise_path(@enterprise), {class: distributor_link_class(@enterprise)} + %li= link_to "Buy direct from the farm", enterprise_shop_path(@enterprise), {class: distributor_link_class(@enterprise)} - @distributors.each do |distributor| %li= render partial: "shared/distributor", object: distributor diff --git a/app/views/json/partials/_hub.rabl b/app/views/json/partials/_hub.rabl index ed9520b43e..76c6729b37 100644 --- a/app/views/json/partials/_hub.rabl +++ b/app/views/json/partials/_hub.rabl @@ -5,7 +5,7 @@ child suppliers: :producers do attributes :id end node :path do |enterprise| - main_app.shop_enterprise_path(enterprise) + main_app.enterprise_shop_path(enterprise) end node :pickup do |hub| hub.shipping_methods.where(:require_ship_address => false).present? diff --git a/app/views/shared/_account_sidebar.html.haml b/app/views/shared/_account_sidebar.html.haml index 041ac97765..e572979eb1 100644 --- a/app/views/shared/_account_sidebar.html.haml +++ b/app/views/shared/_account_sidebar.html.haml @@ -2,9 +2,9 @@ -#.row -#.panel -#%p - -#%strong= link_to "Manage my account", account_path + -#%strong= link_to "Manage my account", account_path -#- if enterprise_user? - -#%strong= link_to "Enterprise admin", admin_path + -#%strong= link_to "Enterprise admin", admin_path -#- if order = last_completed_order -#%dl -#%dt Current Hub: @@ -13,8 +13,8 @@ -#%dt Last hub: -#%dd -#- if order.distributor != current_distributor - -#= link_to "#{order.distributor.name}".html_safe, "", + -#= link_to "#{order.distributor.name}".html_safe, "", -#{class: distributor_link_class(order.distributor), - -#"ng-click" => "emptyCart('#{main_app.shop_enterprise_path(order.distributor)}', $event)"} + -#"ng-click" => "emptyCart('#{main_app.enterprise_shop_path(order.distributor)}', $event)"} -#- else -#= order.distributor.name diff --git a/app/views/shared/_distributor.html.haml b/app/views/shared/_distributor.html.haml index 2712e522a1..ea1eeae0a4 100644 --- a/app/views/shared/_distributor.html.haml +++ b/app/views/shared/_distributor.html.haml @@ -1,3 +1,3 @@ = succeed ',' do - = link_to "#{distributor.name}".html_safe, shop_enterprise_path(distributor), {class: distributor_link_class(distributor)} + = link_to "#{distributor.name}".html_safe, enterprise_shop_path(distributor), {class: distributor_link_class(distributor)} %span.secondary= distributor.city From 9b78963d5ac9b0d154cac3aec44e4418ba6b3cac Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 16 Jan 2015 17:23:47 +1100 Subject: [PATCH 546/681] Adding a javascript interface for checking permalinks and user input on enterprise console --- .../permalink_controller.js.coffee | 17 +++++++ .../services/permalink_checker.js.coffee | 11 +++++ .../admin/enterprise_console.css.scss | 14 ++++++ app/controllers/enterprises_controller.rb | 4 +- .../api/admin/enterprise_serializer.rb | 2 +- .../form/_primary_details.html.haml | 28 ++++++++--- .../permalink_controller_spec.js.coffee | 47 +++++++++++++++++++ .../services/permalink_checker_spec.js.coffee | 14 ++++++ 8 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee create mode 100644 app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee create mode 100644 app/assets/stylesheets/admin/enterprise_console.css.scss create mode 100644 spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee create mode 100644 spec/javascripts/unit/admin/enterprises/services/permalink_checker_spec.js.coffee diff --git a/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee new file mode 100644 index 0000000000..68d4424ece --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee @@ -0,0 +1,17 @@ +angular.module("admin.enterprises") + .controller "permalinkCtrl", ($scope, Enterprise, PermalinkChecker) -> + $scope.pristinePermalink = Enterprise.permalink + $scope.availablility = "" + $scope.checking = false + + $scope.$watch "Enterprise.permalink", (newValue, oldValue) -> + if newValue == $scope.pristinePermalink + $scope.availability = "" + else + $scope.checking = true + PermalinkChecker.check(newValue).then (data) -> + $scope.availability = 'Available' + $scope.checking = false + , (data) -> + $scope.availability = 'Unavailable' + $scope.checking = false \ No newline at end of file diff --git a/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee b/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee new file mode 100644 index 0000000000..b67856b91e --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee @@ -0,0 +1,11 @@ +angular.module("admin.enterprises").factory 'PermalinkChecker', ($q, $http) -> + new class PermalinkChecker + check: (permalink) -> + deferred = $q.defer() + $http.get("/enterprises/check_permalink?permalink=#{permalink}", { headers: { 'Accept': 'application/javascript' } } ) + .success( (data) -> + deferred.resolve data + ).error (data) -> + deferred.reject(data) + + deferred.promise diff --git a/app/assets/stylesheets/admin/enterprise_console.css.scss b/app/assets/stylesheets/admin/enterprise_console.css.scss new file mode 100644 index 0000000000..afca3d9261 --- /dev/null +++ b/app/assets/stylesheets/admin/enterprise_console.css.scss @@ -0,0 +1,14 @@ +span.unavailable, span.available { + font-weight: bold; + i { + font-size: 150%; + } +} + +span.available { + color: #9fc820; +} + +span.unavailable { + color: #DA5354; +} \ No newline at end of file diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 4414dd4ece..8b42ee3743 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -75,11 +75,11 @@ class EnterprisesController < BaseController path = Rails.application.routes.recognize_path( "/#{ params[:permalink].to_s }" ) if path && path[:controller] == "cms_content" respond_to do |format| - format.js { render nothing: true, status: 200 } + format.js { render text: params[:permalink], status: 200 } end else respond_to do |format| - format.js { render nothing: true, status: 409 } + format.js { render text: params[:permalink], status: 409 } end end end diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 14fdd5c8b6..d8b3a54bc2 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -1,5 +1,5 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids - attributes :producer_profile_only, :email, :long_description + attributes :producer_profile_only, :email, :long_description, :permalink attributes :preferred_shopfront_message, :preferred_shopfront_closed_message, :preferred_shopfront_taxon_order end \ No newline at end of file diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml index e42e7038e2..243f6076fb 100644 --- a/app/views/admin/enterprises/form/_primary_details.html.haml +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -66,10 +66,24 @@ = f.radio_button :visible, false   = f.label :visible, "Not Visible", :value => "false" -.row{ ng: { show: "Enterprise.sells == 'own' || Enterprise.sells == 'any'" } } - .three.columns.alpha - %label Link to shop front - .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} - %a What's this? - .eight.columns.omega - = main_app.enterprise_shop_url(@enterprise) \ No newline at end of file +.permalink{ ng: { controller: "permalinkCtrl" } } + .row{ ng: { show: "Enterprise.sells == 'own' || Enterprise.sells == 'any'" } } + .three.columns.alpha + = f.label :permalink, 'Permalink (no spaces)' + .with-tip{'data-powertip' => "This permalink is used to create the url to your shop: #{spree.root_url}your-shop-name/shop"} + %a What's this? + .six.columns + = f.text_field :permalink, { 'ng-model' => "Enterprise.permalink", placeholder: "eg. your-shop-name", 'ng-model-options' => "{ updateOn: 'default blur', debounce: {'default': 300, 'blur': 0} }" } + .two.columns.omega + %img.spinner{ src: "/assets/loading.gif", width: "30px", ng: { show: "checking" } } + %span{ ng: { class: 'availability.toLowerCase()', hide: "checking" } } + {{ availability }} + %i{ ng: { class: "{'icon-ok-sign': availability == 'Available', 'icon-remove-sign': availability == 'Unavailable'}" } } + .row{ ng: { show: "Enterprise.sells == 'own' || Enterprise.sells == 'any'" } } + .three.columns.alpha + %label Link to shop front + .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} + %a What's this? + .eight.columns.omega + = surround spree.root_url, "/shop" do + {{Enterprise.permalink}} \ No newline at end of file diff --git a/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee new file mode 100644 index 0000000000..d95d36327d --- /dev/null +++ b/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee @@ -0,0 +1,47 @@ +describe "permalinkCtrl", -> + ctrl = null + $scope = null + Enterprise = null + PermalinkChecker = null + $httpBackend = null + $q = null + + + beforeEach -> + module('admin.enterprises') + Enterprise = { + permalink: "something" + } + + inject ($rootScope, $controller, _$q_, _PermalinkChecker_) -> + $scope = $rootScope + $q = _$q_ + PermalinkChecker = _PermalinkChecker_ + $ctrl = $controller 'permalinkCtrl', {$scope: $scope, Enterprise: Enterprise, PermalinkChecker: PermalinkChecker} + + describe "checking permalink", -> + deferred = null + beforeEach -> + # Build a deferred object + deferred = $q.defer() + + it "sends a request to PermalinkChecker when permalink is changed", -> + deferred.resolve("") + promise = deferred.promise + spyOn(PermalinkChecker, "check").andReturn promise + $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink + expect(PermalinkChecker.check).toHaveBeenCalled() + + it "sets available to 'Available' when PermalinkChecker resolves", -> + deferred.resolve("") + promise = deferred.promise + spyOn(PermalinkChecker, "check").andReturn promise + $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink + expect($scope.availability).toEqual "Available" + + it "sets available to 'Unavailable' when PermalinkChecker rejects", -> + deferred.reject() + promise = deferred.promise + spyOn(PermalinkChecker, "check").andReturn promise + $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink + expect($scope.availability).toEqual "Unavailable" diff --git a/spec/javascripts/unit/admin/enterprises/services/permalink_checker_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/permalink_checker_spec.js.coffee new file mode 100644 index 0000000000..6eef847858 --- /dev/null +++ b/spec/javascripts/unit/admin/enterprises/services/permalink_checker_spec.js.coffee @@ -0,0 +1,14 @@ +describe "Permalink Checker service", -> + PermalinkChecker = null + $httpBackend = null + beforeEach -> + module 'admin.enterprises' + + inject ($injector, _$httpBackend_) -> + $httpBackend = _$httpBackend_ + PermalinkChecker = $injector.get("PermalinkChecker") + + it "sends an http request to check the permalink", -> + permalink = "this-is-a-permalink" + $httpBackend.expectGET "/enterprises/check_permalink?permalink=#{permalink}" + PermalinkChecker.check(permalink) \ No newline at end of file From 4088bdc2364371b7751109a321b00a12731f02a4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 16 Jan 2015 17:56:24 +1100 Subject: [PATCH 547/681] Clean up permalink checker --- .../controllers/permalink_controller.js.coffee | 4 ++-- app/controllers/enterprises_controller.rb | 12 ++++++------ .../controllers/permalink_controller_spec.js.coffee | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee index 68d4424ece..4201fc3be1 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee @@ -1,6 +1,6 @@ angular.module("admin.enterprises") - .controller "permalinkCtrl", ($scope, Enterprise, PermalinkChecker) -> - $scope.pristinePermalink = Enterprise.permalink + .controller "permalinkCtrl", ($scope, PermalinkChecker) -> + $scope.pristinePermalink = $scope.Enterprise.permalink $scope.availablility = "" $scope.checking = false diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 8b42ee3743..99b5890ec8 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -5,6 +5,8 @@ class EnterprisesController < BaseController before_filter :set_order_cycles, only: :shop before_filter :load_active_distributors, only: :shop + respond_to :js, only: :permalink_checker + def index @enterprises = Enterprise.all end @@ -72,15 +74,13 @@ class EnterprisesController < BaseController end def check_permalink + return render text: params[:permalink], status: 409 if Enterprise.find_by_permalink params[:permalink] + path = Rails.application.routes.recognize_path( "/#{ params[:permalink].to_s }" ) if path && path[:controller] == "cms_content" - respond_to do |format| - format.js { render text: params[:permalink], status: 200 } - end + render text: params[:permalink], status: 200 else - respond_to do |format| - format.js { render text: params[:permalink], status: 409 } - end + render text: params[:permalink], status: 409 end end diff --git a/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee index d95d36327d..959454eab3 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee @@ -9,15 +9,15 @@ describe "permalinkCtrl", -> beforeEach -> module('admin.enterprises') - Enterprise = { + Enterprise = permalink: "something" - } inject ($rootScope, $controller, _$q_, _PermalinkChecker_) -> $scope = $rootScope + $scope.Enterprise = Enterprise $q = _$q_ PermalinkChecker = _PermalinkChecker_ - $ctrl = $controller 'permalinkCtrl', {$scope: $scope, Enterprise: Enterprise, PermalinkChecker: PermalinkChecker} + $ctrl = $controller 'permalinkCtrl', {$scope: $scope, PermalinkChecker: PermalinkChecker} describe "checking permalink", -> deferred = null From 6b10a4a775c8c0c3fc8b6100cbf6d17cea6a7109 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 16 Jan 2015 18:17:15 +1100 Subject: [PATCH 548/681] Restoring enterprise permalinks when they cause errors --- app/models/enterprise.rb | 7 +++++++ spec/models/enterprise_spec.rb | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index fbb525cfe8..31d306f123 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -72,6 +72,8 @@ class Enterprise < ActiveRecord::Base after_update :welcome_after_confirm, if: lambda { confirmation_token_changed? && confirmation_token.nil? } after_create :send_welcome_email, if: lambda { email_is_known? } + after_rollback :restore_permalink + scope :by_name, order('name') scope :visible, where(:visible => true) scope :confirmed, where('confirmed_at IS NOT NULL') @@ -363,4 +365,9 @@ class Enterprise < ActiveRecord::Base errors.add(:shopfront_category_ordering, "must contain a list of taxons.") end end + + def restore_permalink + # If the permalink has errors, reset it to it's original value, so we can update the form + self.permalink = permalink_was if permalink_changed? && errors[:permalink].present? + end end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 9edc1d2cee..8b3a8ab443 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -223,6 +223,16 @@ describe Enterprise do it { should delegate(:state_name).to(:address) } end + describe "callbacks" do + it "restores permalink to original value when it is changed and invalid" do + e1 = create(:enterprise, permalink: "taken") + e2 = create(:enterprise, permalink: "not_taken") + e2.permalink = "taken" + e2.save + expect(e2.permalink).to eq "not_taken" + end + end + describe "scopes" do describe 'visible' do it 'find visible enterprises' do From 7ad9fdf0be04fa0b6948ec374216db7dcce3f803 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 08:24:12 +1100 Subject: [PATCH 549/681] Cleaning permalink before checking --- app/controllers/enterprises_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 99b5890ec8..2271c02def 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -4,6 +4,7 @@ class EnterprisesController < BaseController include OrderCyclesHelper before_filter :set_order_cycles, only: :shop before_filter :load_active_distributors, only: :shop + before_filter :clean_permalink, only: :check_permalink respond_to :js, only: :permalink_checker @@ -86,6 +87,10 @@ class EnterprisesController < BaseController private + def clean_permalink + params[:permalink] = params[:permalink].delete "^a-zA-Z1-9-_" + end + def set_order_cycles @order_cycles = OrderCycle.with_distributor(@distributor).active From 83726eba637737d6edb2199ede28041d61324e6f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 08:24:43 +1100 Subject: [PATCH 550/681] Refactoring permalink checker to handle multiple requests elegantly --- .../permalink_controller.js.coffee | 28 ++++++++----- .../services/permalink_checker.js.coffee | 41 +++++++++++++++---- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee index 4201fc3be1..a842209dbc 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/permalink_controller.js.coffee @@ -1,17 +1,23 @@ angular.module("admin.enterprises") .controller "permalinkCtrl", ($scope, PermalinkChecker) -> - $scope.pristinePermalink = $scope.Enterprise.permalink + # locals + initialPermalink = $scope.Enterprise.permalink + pendingRequest = null + + # variables on $scope $scope.availablility = "" $scope.checking = false $scope.$watch "Enterprise.permalink", (newValue, oldValue) -> - if newValue == $scope.pristinePermalink - $scope.availability = "" - else - $scope.checking = true - PermalinkChecker.check(newValue).then (data) -> - $scope.availability = 'Available' - $scope.checking = false - , (data) -> - $scope.availability = 'Unavailable' - $scope.checking = false \ No newline at end of file + $scope.checking = true + pendingRequest = PermalinkChecker.check(newValue) + + pendingRequest.then (data) -> + if data.permalink == initialPermalink + $scope.availability = "" + else + $scope.availability = data.available + $scope.Enterprise.permalink = data.permalink + $scope.checking = false + , (data) -> + # Do nothing (this is hopefully an aborted request) diff --git a/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee b/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee index b67856b91e..cb3fde9324 100644 --- a/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee +++ b/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee @@ -1,11 +1,36 @@ angular.module("admin.enterprises").factory 'PermalinkChecker', ($q, $http) -> new class PermalinkChecker - check: (permalink) -> - deferred = $q.defer() - $http.get("/enterprises/check_permalink?permalink=#{permalink}", { headers: { 'Accept': 'application/javascript' } } ) - .success( (data) -> - deferred.resolve data - ).error (data) -> - deferred.reject(data) + deferredRequest: null + deferredAbort: null - deferred.promise + check: (permalink) => + @abort(@deferredAbort) if @deferredRequest && @deferredRequest.promise + @deferredRequest = deferredRequest = $q.defer() + @deferredAbort = deferredAbort = $q.defer() + request = $http( + method: "GET" + url: "/enterprises/check_permalink?permalink=#{permalink}" + headers: + Accept: 'application/javascript' + timeout: deferredAbort.promise + ) + .success( (data) => + deferredRequest.resolve + permalink: data + available: "Available" + ).error (data,status) => + if status == 409 + deferredRequest.resolve + permalink: data + available: "Unavailable" + else + # Something went wrong or request was aborted + deferredRequest.reject() + + deferredRequest.promise.finally -> + request = deferredRequest.promise = null; + + deferredRequest.promise + + abort: (deferredAbort) -> + deferredAbort.resolve() From 585c061fb0bc97e0782c1586798872c6c0f584d6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 08:48:31 +1100 Subject: [PATCH 551/681] Upgrade angularjs --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d56a77c284..416faab0d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -154,7 +154,7 @@ GEM sprockets tilt angularjs-file-upload-rails (1.1.0) - angularjs-rails (1.2.13) + angularjs-rails (1.3.9) ansi (1.4.2) arel (3.0.3) awesome_nested_set (2.1.5) From a0990c107ff9cfcfeec208bb00b76ac0023ab5cd Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 09:00:17 +1100 Subject: [PATCH 552/681] Moving navigation check to controller --- .../controllers/enterprise_controller.js.coffee | 5 ++++- .../admin/utils/directives/navigation_check.js.coffee | 10 ---------- app/views/admin/enterprises/_ng_form.html.haml | 4 +--- 3 files changed, 5 insertions(+), 14 deletions(-) delete mode 100644 app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index d218f7caea..147c08943d 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -11,10 +11,13 @@ angular.module("admin.enterprises") # Provide a callback for generating warning messages displayed before leaving the page. This is passed in # from a directive "nav-check" in the page - if we pass it here it will be called in the test suite, # and on all new uses of this contoller, and we might not want that . - $scope.enterpriseNavCallback = -> + enterpriseNavCallback = -> if $scope.enterprise.$dirty "Your changes to the enterprise are not saved yet." + # Register the NavigationCheck callback + NavigationCheck.register(enterpriseNavCallback) + for payment_method in $scope.PaymentMethods payment_method.selected = payment_method.id in $scope.Enterprise.payment_method_ids diff --git a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee b/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee deleted file mode 100644 index fee80e9acf..0000000000 --- a/app/assets/javascripts/admin/utils/directives/navigation_check.js.coffee +++ /dev/null @@ -1,10 +0,0 @@ -angular.module("admin.utils").directive "navCheckCallback", (NavigationCheck)-> - restrict: 'A' - scope: - navCheckCallback: '&' - link: (scope,element,attributes) -> - # Provide a callback, otherwise this default will be used: - callback = scope.navCheckCallback() - callback ||= -> - "You will lose any unsaved work!" - NavigationCheck.register(callback) diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 76ec768a3a..91a3c4e3aa 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -1,12 +1,10 @@ --# Not all inputs are ng inputs, they don't make the form dirty on change. +-# Not all inputs are ng inputs, they don't make the ng-form dirty on change. -# ng-change is only valid for inputs, not for a form. -# So we use onchange and have to get the scope to access the ng controller --# The nav-check-callback is warning on leave if the form is dirty. = form_for [main_app, :admin, @enterprise], html: { name: "enterprise", "ng-app" => 'admin.enterprises', "ng-submit" => "navClear()", "ng-controller" => 'enterpriseCtrl', - "nav-check-callback" => 'enterpriseNavCallback', 'onchange' => 'angular.element(enterprise).scope().enterprise.$setDirty()', } do |f| .row From 48dc85cfc2af70265131fed3aa7cb157ddf0d17e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 09:22:04 +1100 Subject: [PATCH 553/681] Update bindonce --- app/assets/javascripts/shared/bindonce.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/shared/bindonce.min.js b/app/assets/javascripts/shared/bindonce.min.js index 0edd3d57d4..8c118f69e3 100644 --- a/app/assets/javascripts/shared/bindonce.min.js +++ b/app/assets/javascripts/shared/bindonce.min.js @@ -1 +1 @@ -!function(){"use strict";var e=angular.module("pasvaz.bindonce",[]);e.directive("bindonce",function(){var e=function(e){if(e&&0!==e.length){var t=angular.lowercase(""+e);e=!("f"===t||"0"===t||"false"===t||"no"===t||"n"===t||"[]"===t)}else e=!1;return e},t=parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10);isNaN(t)&&(t=parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10));var r={restrict:"AM",controller:["$scope","$element","$attrs","$interpolate",function(r,a,i,n){var c=function(t,r,a){var i="show"===r?"":"none",n="hide"===r?"":"none";t.css("display",e(a)?i:n)},o=function(e,t){if(angular.isObject(t)&&!angular.isArray(t)){var r=[];angular.forEach(t,function(e,t){e&&r.push(t)}),t=r}t&&e.addClass(angular.isArray(t)?t.join(" "):t)},s=function(e,t){e.transclude(t,function(t){var r=e.element.parent(),a=e.element&&e.element[e.element.length-1],i=r&&r[0]||a&&a.parentNode,n=a&&a.nextSibling||null;angular.forEach(t,function(e){i.insertBefore(e,n)})})},l={watcherRemover:void 0,binders:[],group:i.boName,element:a,ran:!1,addBinder:function(e){this.binders.push(e),this.ran&&this.runBinders()},setupWatcher:function(e){var t=this;this.watcherRemover=r.$watch(e,function(e){void 0!==e&&(t.removeWatcher(),t.checkBindonce(e))},!0)},checkBindonce:function(e){var t=this,r=e.$promise?e.$promise.then:e.then;"function"==typeof r?r(function(){t.runBinders()}):t.runBinders()},removeWatcher:function(){void 0!==this.watcherRemover&&(this.watcherRemover(),this.watcherRemover=void 0)},runBinders:function(){for(;this.binders.length>0;){var r=this.binders.shift();if(!this.group||this.group==r.group){var a=r.scope.$eval(r.interpolate?n(r.value):r.value);switch(r.attr){case"boIf":e(a)&&s(r,r.scope.$new());break;case"boSwitch":var i,l=r.controller[0];(i=l.cases["!"+a]||l.cases["?"])&&(r.scope.$eval(r.attrs.change),angular.forEach(i,function(e){s(e,r.scope.$new())}));break;case"boSwitchWhen":var u=r.controller[0];u.cases["!"+r.attrs.boSwitchWhen]=u.cases["!"+r.attrs.boSwitchWhen]||[],u.cases["!"+r.attrs.boSwitchWhen].push({transclude:r.transclude,element:r.element});break;case"boSwitchDefault":var u=r.controller[0];u.cases["?"]=u.cases["?"]||[],u.cases["?"].push({transclude:r.transclude,element:r.element});break;case"hide":case"show":c(r.element,r.attr,a);break;case"class":o(r.element,a);break;case"text":r.element.text(a);break;case"html":r.element.html(a);break;case"style":r.element.css(a);break;case"src":r.element.attr(r.attr,a),t&&r.element.prop("src",a);break;case"attr":angular.forEach(r.attrs,function(e,t){var a,i;t.match(/^boAttr./)&&r.attrs[t]&&(a=t.replace(/^boAttr/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=r.scope.$eval(r.attrs[t]),r.element.attr(a,i))});break;case"href":case"alt":case"title":case"id":case"value":r.element.attr(r.attr,a)}}}this.ran=!0}};return l}],link:function(e,t,r,a){var i=r.bindonce&&e.$eval(r.bindonce);void 0!==i?a.checkBindonce(i):(a.setupWatcher(r.bindonce),t.bind("$destroy",a.removeWatcher))}};return r}),angular.forEach([{directiveName:"boShow",attribute:"show"},{directiveName:"boHide",attribute:"hide"},{directiveName:"boClass",attribute:"class"},{directiveName:"boText",attribute:"text"},{directiveName:"boBind",attribute:"text"},{directiveName:"boHtml",attribute:"html"},{directiveName:"boSrcI",attribute:"src",interpolate:!0},{directiveName:"boSrc",attribute:"src"},{directiveName:"boHrefI",attribute:"href",interpolate:!0},{directiveName:"boHref",attribute:"href"},{directiveName:"boAlt",attribute:"alt"},{directiveName:"boTitle",attribute:"title"},{directiveName:"boId",attribute:"id"},{directiveName:"boStyle",attribute:"style"},{directiveName:"boValue",attribute:"value"},{directiveName:"boAttr",attribute:"attr"},{directiveName:"boIf",transclude:"element",terminal:!0,priority:1e3},{directiveName:"boSwitch",require:"boSwitch",controller:function(){this.cases={}}},{directiveName:"boSwitchWhen",transclude:"element",priority:800,require:"^boSwitch"},{directiveName:"boSwitchDefault",transclude:"element",priority:800,require:"^boSwitch"}],function(t){var r=200;return e.directive(t.directiveName,function(){var e={priority:t.priority||r,transclude:t.transclude||!1,terminal:t.terminal||!1,require:["^bindonce"].concat(t.require||[]),controller:t.controller,compile:function(e,r,a){return function(e,r,i,n){var c=n[0],o=i.boParent;if(o&&c.group!==o){var s=c.element.parent();c=void 0;for(var l;9!==s[0].nodeType&&s.length;){if((l=s.data("$bindonceController"))&&l.group===o){c=l;break}s=s.parent()}if(!c)throw new Error("No bindonce controller: "+o)}c.addBinder({element:r,attr:t.attribute||t.directiveName,attrs:i,value:i[t.directiveName],interpolate:t.interpolate,group:o,transclude:a,controller:n.slice(1),scope:e})}}};return e})})}(); \ No newline at end of file +!function(){"use strict";var e=angular.module("pasvaz.bindonce",[]);e.directive("bindonce",function(){var e=function(e){if(e&&0!==e.length){var t=angular.lowercase(""+e);e=!("f"===t||"0"===t||"false"===t||"no"===t||"n"===t||"[]"===t)}else e=!1;return e},t=parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10);isNaN(t)&&(t=parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10));var r={restrict:"AM",controller:["$scope","$element","$attrs","$interpolate",function(r,a,i,n){var c=function(t,r,a){var i="show"===r?"":"none",n="hide"===r?"":"none";t.css("display",e(a)?i:n)},o=function(e,t){if(angular.isObject(t)&&!angular.isArray(t)){var r=[];angular.forEach(t,function(e,t){e&&r.push(t)}),t=r}t&&e.addClass(angular.isArray(t)?t.join(" "):t)},s=function(e,t){e.transclude(t,function(t){var r=e.element.parent(),a=e.element&&e.element[e.element.length-1],i=r&&r[0]||a&&a.parentNode,n=a&&a.nextSibling||null;angular.forEach(t,function(e){i.insertBefore(e,n)})})},l={watcherRemover:void 0,binders:[],group:i.boName,element:a,ran:!1,addBinder:function(e){this.binders.push(e),this.ran&&this.runBinders()},setupWatcher:function(e){var t=this;this.watcherRemover=r.$watch(e,function(e){void 0!==e&&(t.removeWatcher(),t.checkBindonce(e))},!0)},checkBindonce:function(e){var t=this,r=e.$promise?e.$promise.then:e.then;"function"==typeof r?r(function(){t.runBinders()}):t.runBinders()},removeWatcher:function(){void 0!==this.watcherRemover&&(this.watcherRemover(),this.watcherRemover=void 0)},runBinders:function(){for(;this.binders.length>0;){var r=this.binders.shift();if(!this.group||this.group==r.group){var a=r.scope.$eval(r.interpolate?n(r.value):r.value);switch(r.attr){case"boIf":e(a)&&s(r,r.scope.$new());break;case"boSwitch":var i,l=r.controller[0];(i=l.cases["!"+a]||l.cases["?"])&&(r.scope.$eval(r.attrs.change),angular.forEach(i,function(e){s(e,r.scope.$new())}));break;case"boSwitchWhen":var u=r.controller[0];u.cases["!"+r.attrs.boSwitchWhen]=u.cases["!"+r.attrs.boSwitchWhen]||[],u.cases["!"+r.attrs.boSwitchWhen].push({transclude:r.transclude,element:r.element});break;case"boSwitchDefault":var u=r.controller[0];u.cases["?"]=u.cases["?"]||[],u.cases["?"].push({transclude:r.transclude,element:r.element});break;case"hide":case"show":c(r.element,r.attr,a);break;case"class":o(r.element,a);break;case"text":r.element.text(a);break;case"html":r.element.html(a);break;case"style":r.element.css(a);break;case"disabled":r.element.prop("disabled",a);break;case"src":r.element.attr(r.attr,a),t&&r.element.prop("src",a);break;case"attr":angular.forEach(r.attrs,function(e,t){var a,i;t.match(/^boAttr./)&&r.attrs[t]&&(a=t.replace(/^boAttr/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=r.scope.$eval(r.attrs[t]),r.element.attr(a,i))});break;case"href":case"alt":case"title":case"id":case"value":r.element.attr(r.attr,a)}}}this.ran=!0}};angular.extend(this,l)}],link:function(e,t,r,a){var i=r.bindonce&&e.$eval(r.bindonce);void 0!==i?a.checkBindonce(i):(a.setupWatcher(r.bindonce),t.bind("$destroy",a.removeWatcher))}};return r}),angular.forEach([{directiveName:"boShow",attribute:"show"},{directiveName:"boHide",attribute:"hide"},{directiveName:"boClass",attribute:"class"},{directiveName:"boText",attribute:"text"},{directiveName:"boBind",attribute:"text"},{directiveName:"boHtml",attribute:"html"},{directiveName:"boSrcI",attribute:"src",interpolate:!0},{directiveName:"boSrc",attribute:"src"},{directiveName:"boHrefI",attribute:"href",interpolate:!0},{directiveName:"boHref",attribute:"href"},{directiveName:"boAlt",attribute:"alt"},{directiveName:"boTitle",attribute:"title"},{directiveName:"boId",attribute:"id"},{directiveName:"boStyle",attribute:"style"},{directiveName:"boDisabled",attribute:"disabled"},{directiveName:"boValue",attribute:"value"},{directiveName:"boAttr",attribute:"attr"},{directiveName:"boIf",transclude:"element",terminal:!0,priority:1e3},{directiveName:"boSwitch",require:"boSwitch",controller:function(){this.cases={}}},{directiveName:"boSwitchWhen",transclude:"element",priority:800,require:"^boSwitch"},{directiveName:"boSwitchDefault",transclude:"element",priority:800,require:"^boSwitch"}],function(t){var r=200;return e.directive(t.directiveName,function(){var e={priority:t.priority||r,transclude:t.transclude||!1,terminal:t.terminal||!1,require:["^bindonce"].concat(t.require||[]),controller:t.controller,compile:function(e,r,a){return function(e,r,i,n){var c=n[0],o=i.boParent;if(o&&c.group!==o){var s=c.element.parent();c=void 0;for(var l;9!==s[0].nodeType&&s.length;){if((l=s.data("$bindonceController"))&&l.group===o){c=l;break}s=s.parent()}if(!c)throw new Error("No bindonce controller: "+o)}c.addBinder({element:r,attr:t.attribute||t.directiveName,attrs:i,value:i[t.directiveName],interpolate:t.interpolate,group:o,transclude:a,controller:n.slice(1),scope:e})}}};return e})})}(); From ff624e83eb28150efe8965cd08b4d1e2ce0109be Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 09:22:28 +1100 Subject: [PATCH 554/681] Require distributor in enterprise controller, so that everything actually works --- app/controllers/enterprises_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 2271c02def..afab8f398d 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -2,6 +2,7 @@ class EnterprisesController < BaseController layout "darkswarm" helper Spree::ProductsHelper include OrderCyclesHelper + before_filter :require_distributor_chosen before_filter :set_order_cycles, only: :shop before_filter :load_active_distributors, only: :shop before_filter :clean_permalink, only: :check_permalink From b98c01b280c5da3d5c96e39a5dd1869c46d73791 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 10:53:02 +1100 Subject: [PATCH 555/681] Updating permalink controller specs --- .../permalink_controller_spec.js.coffee | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee index 959454eab3..888a42daa3 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee @@ -32,16 +32,26 @@ describe "permalinkCtrl", -> $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink expect(PermalinkChecker.check).toHaveBeenCalled() - it "sets available to 'Available' when PermalinkChecker resolves", -> - deferred.resolve("") + it "sets available to '' when PermalinkChecker resolves permalink to the existing permalink on Enterprise ", -> + deferred.resolve({permalink: "something"}) promise = deferred.promise spyOn(PermalinkChecker, "check").andReturn promise $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink + expect($scope.availability).toEqual "" + + it "sets available and permalink when PermalinkChecker resolves", -> + deferred.resolve({ available: "Available", permalink: "permalink"}) + promise = deferred.promise + spyOn(PermalinkChecker, "check").andReturn promise + $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink + expect(Enterprise.permalink).toEqual "permalink" expect($scope.availability).toEqual "Available" - it "sets available to 'Unavailable' when PermalinkChecker rejects", -> + it "does nothing when PermalinkChecker rejects", -> + $scope.availability = "Some Availability" deferred.reject() promise = deferred.promise spyOn(PermalinkChecker, "check").andReturn promise $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink - expect($scope.availability).toEqual "Unavailable" + expect($scope.availability).toEqual "Some Availability" + expect(Enterprise.permalink).toEqual "somethingelse" From 3e5ea3fe6316e38439434bfe102cbc0f1194df5d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 14:28:58 +1100 Subject: [PATCH 556/681] Adding automatic permalink generation on enterprise creation --- app/models/enterprise.rb | 20 ++++++++++++++++++ spec/models/enterprise_spec.rb | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 31d306f123..81a00d4982 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -63,6 +63,7 @@ class Enterprise < ActiveRecord::Base before_save :confirmation_check, if: lambda { email_changed? } + before_validation :initialize_permalink, if: lambda { permalink.nil? } before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? } before_validation :set_unused_address_fields after_validation :geocode_address @@ -295,6 +296,21 @@ class Enterprise < ActiveRecord::Base shipping_methods.any? && payment_methods.available.any? end + def self.find_available_permalink(test_permalink) + test_permalink = test_permalink.parameterize + existing = Enterprise.select(:permalink).order(:permalink).where("permalink LIKE ?", "#{test_permalink}%").map(&:permalink) + if existing.empty? + test_permalink + else + used_indices = existing.map do |p| + p.slice!(/^#{test_permalink}/) + p.match(/^\d+$/).to_s.to_i + end.select{ |p| p } + options = (1..existing.length).to_a - used_indices + test_permalink + options.first.to_s + end + end + protected def devise_mailer @@ -370,4 +386,8 @@ class Enterprise < ActiveRecord::Base # If the permalink has errors, reset it to it's original value, so we can update the form self.permalink = permalink_was if permalink_changed? && errors[:permalink].present? end + + def initialize_permalink + self.permalink = Enterprise.find_available_permalink(name) + end end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 8b3a8ab443..3e7c6f84d9 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -802,4 +802,42 @@ describe Enterprise do non_producer_sell_none.category.should == :hub_profile end end + + describe "finding and automatically assigning a permalink" do + let(:enterprise) { build(:enterprise, name: "Name To Turn Into A Permalink") } + it "assigns permalink when initialized" do + allow(Enterprise).to receive(:find_available_permalink).and_return("available_permalink") + Enterprise.should_receive(:find_available_permalink).with("Name To Turn Into A Permalink") + expect( + lambda { enterprise.send(:initialize_permalink) } + ).to change{ + enterprise.permalink + }.to( + "available_permalink" + ) + end + + describe "finding a permalink" do + let!(:enterprise1) { create(:enterprise, permalink: "permalink") } + let!(:enterprise2) { create(:enterprise, permalink: "permalink1") } + + it "parameterizes the value provided" do + expect(Enterprise.find_available_permalink("Some Unused Permalink")).to eq "some-unused-permalink" + end + + it "finds and index value based on existing permalinks" do + expect(Enterprise.find_available_permalink("permalink")).to eq "permalink2" + end + + it "ignores permalinks with characters after the index value" do + create(:enterprise, permalink: "permalink2xxx") + expect(Enterprise.find_available_permalink("permalink")).to eq "permalink2" + end + + it "finds gaps in the indices of existing permalinks" do + create(:enterprise, permalink: "permalink3") + expect(Enterprise.find_available_permalink("permalink")).to eq "permalink2" + end + end + end end From 60313f7a6a04b614b6e5d5b9b2fdce106fb17817 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 14:33:19 +1100 Subject: [PATCH 557/681] Replace delete with parameterize --- app/controllers/enterprises_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index afab8f398d..4f244bf49f 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -89,7 +89,7 @@ class EnterprisesController < BaseController private def clean_permalink - params[:permalink] = params[:permalink].delete "^a-zA-Z1-9-_" + params[:permalink] = params[:permalink].parameterize end def set_order_cycles From 4baa205cf959199817733002e7eba50b2c9bbf01 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 16:19:01 +1100 Subject: [PATCH 558/681] before_filters for enterprise controller are run in the correct order, and put inside the shop action --- app/controllers/enterprises_controller.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 4f244bf49f..d8b33e4ad6 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -2,9 +2,6 @@ class EnterprisesController < BaseController layout "darkswarm" helper Spree::ProductsHelper include OrderCyclesHelper - before_filter :require_distributor_chosen - before_filter :set_order_cycles, only: :shop - before_filter :load_active_distributors, only: :shop before_filter :clean_permalink, only: :check_permalink respond_to :js, only: :permalink_checker @@ -73,6 +70,10 @@ class EnterprisesController < BaseController order_cycle_options = OrderCycle.active.with_distributor(distributor) order.order_cycle = order_cycle_options.first if order_cycle_options.count == 1 order.save! + + require_distributor_chosen + set_order_cycles + load_active_distributors end def check_permalink From 177181cd754733d71cd8063679a25844d62b4ed5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 16:24:05 +1100 Subject: [PATCH 559/681] CurrentUser always returns a factory --- .../controllers/checkout/checkout_controller.js.coffee | 4 ++-- .../darkswarm/services/current_user.js.coffee | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee index 950be00bae..1197535033 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee @@ -4,7 +4,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, storage, Checkout, CurrentUser, Cu # Bind to local storage $scope.fieldsToBind = ["bill_address", "email", "payment_method_id", "shipping_method_id", "ship_address"] - prefix = "order_#{Checkout.order.id}#{CurrentUser?.id}#{CurrentHub.hub.id}" + prefix = "order_#{Checkout.order.id}#{CurrentUser.id or ""}#{CurrentHub.hub.id}" for field in $scope.fieldsToBind storage.bind $scope, "Checkout.order.#{field}", @@ -16,7 +16,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, storage, Checkout, CurrentUser, Cu $scope.order = Checkout.order # Ordering is important $scope.secrets = Checkout.secrets - $scope.enabled = !!CurrentUser + $scope.enabled = !!CurrentUser.id? $scope.purchase = (event, form) -> event.preventDefault() diff --git a/app/assets/javascripts/darkswarm/services/current_user.js.coffee b/app/assets/javascripts/darkswarm/services/current_user.js.coffee index 382324e22c..0098efaa2b 100644 --- a/app/assets/javascripts/darkswarm/services/current_user.js.coffee +++ b/app/assets/javascripts/darkswarm/services/current_user.js.coffee @@ -1,7 +1,4 @@ Darkswarm.factory 'CurrentUser', (user)-> # This is for the current user - if user and !$.isEmptyObject(user) - new class CurrentUser - constructor: -> - @[k] = v for k, v of user - else - undefined + new class CurrentUser + constructor: -> + @[k] = v for k, v of user if user and !$.isEmptyObject(user) From 4d26b3d6484cfd9c8cfed0b68dd03e0db27d278d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 16:55:53 +1100 Subject: [PATCH 560/681] Make unused cart_controller happy --- app/controllers/open_food_network/cart_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/open_food_network/cart_controller.rb b/app/controllers/open_food_network/cart_controller.rb index 38489ce849..a4bc1c7b76 100644 --- a/app/controllers/open_food_network/cart_controller.rb +++ b/app/controllers/open_food_network/cart_controller.rb @@ -20,7 +20,7 @@ module OpenFoodNetwork def add_variant @cart = Cart.find(params[:cart_id]) - distributor = Enterprise.find(params[:distributor_id]) + distributor = Enterprise.find_by_permalink(params[:distributor_id]) order_cycle = OrderCycle.find(params[:order_cycle_id]) if params[:order_cycle_id] if @cart.add_variant params[:variant_id], params[:quantity], distributor, order_cycle, current_currency @@ -36,4 +36,4 @@ module OpenFoodNetwork Spree::Config[:currency] end end -end \ No newline at end of file +end From 42d66952647fbfe6c59797988dfa95571332e5e8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 17:13:24 +1100 Subject: [PATCH 561/681] Fix permalink migration to handle blank auto-generated permalinks and fixed down migration --- db/migrate/20141219034321_add_permalink_to_enterprises.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/migrate/20141219034321_add_permalink_to_enterprises.rb b/db/migrate/20141219034321_add_permalink_to_enterprises.rb index d233a18d5d..3fe1243065 100644 --- a/db/migrate/20141219034321_add_permalink_to_enterprises.rb +++ b/db/migrate/20141219034321_add_permalink_to_enterprises.rb @@ -5,6 +5,7 @@ class AddPermalinkToEnterprises < ActiveRecord::Migration Enterprise.all.each do |enterprise| counter = 1 permalink = enterprise.name.parameterize + permalink = "my-enterprise-name" if permalink = "" while Enterprise.find_by_permalink(permalink) do permalink = enterprise.name.parameterize + counter.to_s counter += 1 @@ -17,6 +18,6 @@ class AddPermalinkToEnterprises < ActiveRecord::Migration end def down - add_column :enterprises, :permalink + remove_column :enterprises, :permalink end end From 6991e5e6b1800ecda663af33d252698b05f70ec3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Tue, 20 Jan 2015 17:41:01 +1100 Subject: [PATCH 562/681] Finally got all callbacks available to all action that need them --- app/controllers/base_controller.rb | 13 ++++++++++++- app/controllers/enterprises_controller.rb | 9 --------- app/controllers/shop_controller.rb | 1 + 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index 2ac35fa3ad..1d74df5706 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -11,8 +11,19 @@ class BaseController < ApplicationController include Spree::ProductsHelper before_filter :check_order_cycle_expiry - + def load_active_distributors @active_distributors ||= Enterprise.distributors_with_active_order_cycles end + + private + + def set_order_cycles + @order_cycles = OrderCycle.with_distributor(@distributor).active + + # And default to the only order cycle if there's only the one + if @order_cycles.count == 1 + current_order(true).set_order_cycle! @order_cycles.first + end + end end diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index d8b33e4ad6..77cebdb183 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -92,13 +92,4 @@ class EnterprisesController < BaseController def clean_permalink params[:permalink] = params[:permalink].parameterize end - - def set_order_cycles - @order_cycles = OrderCycle.with_distributor(@distributor).active - - # And default to the only order cycle if there's only the one - if @order_cycles.count == 1 - current_order(true).set_order_cycle! @order_cycles.first - end - end end diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index c350791158..73861def5c 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -3,6 +3,7 @@ require 'open_food_network/scope_product_to_hub' class ShopController < BaseController layout "darkswarm" before_filter :require_distributor_chosen + before_filter :set_order_cycles def show redirect_to main_app.enterprise_shop_path(current_distributor) From 7520552fd7ed48310b9a5a825a7ec08708f0a991 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 21 Jan 2015 14:35:18 +1100 Subject: [PATCH 563/681] Removing spec for testing empty permalink, since we now force permalink to be created when empty --- spec/models/enterprise_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 3e7c6f84d9..f6b57b8cc5 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -168,7 +168,6 @@ describe Enterprise do subject { FactoryGirl.create(:distributor_enterprise) } it { should validate_presence_of(:name) } it { should validate_presence_of(:email) } - it { should validate_presence_of(:permalink) } it { should validate_uniqueness_of(:permalink) } it { should ensure_length_of(:description).is_at_most(255) } From 36430d3bad4d33e7cdf124ecc7ec80b2da062484 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 21 Jan 2015 14:40:28 +1100 Subject: [PATCH 564/681] Enterprise permalink generator handles blank permalink case --- app/models/enterprise.rb | 1 + spec/models/enterprise_spec.rb | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 81a00d4982..d126e489ae 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -298,6 +298,7 @@ class Enterprise < ActiveRecord::Base def self.find_available_permalink(test_permalink) test_permalink = test_permalink.parameterize + test_permalink = "my-enterprise" if test_permalink.blank? existing = Enterprise.select(:permalink).order(:permalink).where("permalink LIKE ?", "#{test_permalink}%").map(&:permalink) if existing.empty? test_permalink diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index f6b57b8cc5..f2fc256c16 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -824,6 +824,11 @@ describe Enterprise do expect(Enterprise.find_available_permalink("Some Unused Permalink")).to eq "some-unused-permalink" end + it "sets the permalink to 'my-enterprise' if parametized permalink is blank" do + expect(Enterprise.find_available_permalink("")).to eq "my-enterprise" + expect(Enterprise.find_available_permalink("$$%{$**}$%}")).to eq "my-enterprise" + end + it "finds and index value based on existing permalinks" do expect(Enterprise.find_available_permalink("permalink")).to eq "permalink2" end From 9245af6a8f5f1e65a3fe8372c1afb2c501f1d10f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 21 Jan 2015 14:42:00 +1100 Subject: [PATCH 565/681] Producer properties routes use enterprise permalink --- app/controllers/admin/producer_properties_controller.rb | 2 +- app/views/admin/enterprises/_actions.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/producer_properties_controller.rb b/app/controllers/admin/producer_properties_controller.rb index b297e74ff2..66656d9aa0 100644 --- a/app/controllers/admin/producer_properties_controller.rb +++ b/app/controllers/admin/producer_properties_controller.rb @@ -12,7 +12,7 @@ module Admin end def load_enterprise - @enterprise = Enterprise.find params[:enterprise_id] + @enterprise = Enterprise.find_by_permalink params[:enterprise_id] end def load_properties diff --git a/app/views/admin/enterprises/_actions.html.haml b/app/views/admin/enterprises/_actions.html.haml index 2ca78f75df..ec29607747 100644 --- a/app/views/admin/enterprises/_actions.html.haml +++ b/app/views/admin/enterprises/_actions.html.haml @@ -6,7 +6,7 @@ %br/ - if enterprise.is_primary_producer - = link_to_with_icon 'icon-dashboard', 'Properties', main_app.admin_enterprise_producer_properties_path(enterprise_id: enterprise.id) + = link_to_with_icon 'icon-dashboard', 'Properties', main_app.admin_enterprise_producer_properties_path(enterprise_id: enterprise) (#{enterprise.producer_properties.count}) %br/ From d7ea81e821541e7505ea9df39735f6aaf60c4e79 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 21 Jan 2015 14:46:03 +1100 Subject: [PATCH 566/681] Prevent default form action from submitting ordercycle forms --- .../admin/order_cycle.js.erb.coffee | 6 ++-- .../controllers/simple_create.js.coffee | 3 +- .../controllers/simple_edit.js.coffee | 3 +- .../services/order_cycle.js.coffee | 29 ++++++++++--------- app/views/admin/order_cycles/edit.html.haml | 2 +- app/views/admin/order_cycles/new.html.haml | 2 +- .../unit/order_cycle_spec.js.coffee | 9 +++--- 7 files changed, 30 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/admin/order_cycle.js.erb.coffee b/app/assets/javascripts/admin/order_cycle.js.erb.coffee index 6a7d565bae..608de85d8a 100644 --- a/app/assets/javascripts/admin/order_cycle.js.erb.coffee +++ b/app/assets/javascripts/admin/order_cycle.js.erb.coffee @@ -74,7 +74,8 @@ angular.module('admin.order_cycles', ['ngResource']) $scope.removeDistributionOfVariant = (variant_id) -> OrderCycle.removeDistributionOfVariant(variant_id) - $scope.submit = -> + $scope.submit = (event) -> + event.preventDefault() OrderCycle.create() ]) @@ -154,7 +155,8 @@ angular.module('admin.order_cycles', ['ngResource']) $scope.removeDistributionOfVariant = (variant_id) -> OrderCycle.removeDistributionOfVariant(variant_id) - $scope.submit = -> + $scope.submit = (event) -> + event.preventDefault() OrderCycle.update() ]) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee index e2a60e424e..b93d464d74 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee @@ -38,6 +38,7 @@ angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl $scope.enterpriseFeesForEnterprise = (enterprise_id) -> EnterpriseFee.forEnterprise(parseInt(enterprise_id)) - $scope.submit = -> + $scope.submit = (event) -> + event.preventDefault() OrderCycle.mirrorIncomingToOutgoingProducts() OrderCycle.create() diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee index bcdaa64e91..84f2fcf74a 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee @@ -32,6 +32,7 @@ angular.module('admin.order_cycles').controller "AdminSimpleEditOrderCycleCtrl", $event.preventDefault() OrderCycle.removeCoordinatorFee(index) - $scope.submit = -> + $scope.submit = (event) -> + event.preventDefault() OrderCycle.mirrorIncomingToOutgoingProducts() OrderCycle.update() diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index 687a0164fa..40197452bf 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -87,20 +87,21 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) load: (order_cycle_id, callback=null) -> service = this OrderCycle.get {order_cycle_id: order_cycle_id}, (oc) -> - angular.extend(service.order_cycle, oc) - service.order_cycle.incoming_exchanges = [] - service.order_cycle.outgoing_exchanges = [] - for exchange in service.order_cycle.exchanges - if exchange.incoming - angular.extend(exchange, {enterprise_id: exchange.sender_id, active: true}) - delete(exchange.receiver_id) - service.order_cycle.incoming_exchanges.push(exchange) - - else - angular.extend(exchange, {enterprise_id: exchange.receiver_id, active: true}) - delete(exchange.sender_id) - service.order_cycle.outgoing_exchanges.push(exchange) - + delete oc.$promise + delete oc.$resolved + angular.extend(service.order_cycle, oc) + service.order_cycle.incoming_exchanges = [] + service.order_cycle.outgoing_exchanges = [] + for exchange in service.order_cycle.exchanges + if exchange.incoming + angular.extend(exchange, {enterprise_id: exchange.sender_id, active: true}) + delete(exchange.receiver_id) + service.order_cycle.incoming_exchanges.push(exchange) + else + angular.extend(exchange, {enterprise_id: exchange.receiver_id, active: true}) + delete(exchange.sender_id) + service.order_cycle.outgoing_exchanges.push(exchange) + delete(service.order_cycle.exchanges) service.loaded = true diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index ff40cfbbbc..d4a6751b48 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -2,7 +2,7 @@ - ng_controller = order_cycles_simple_view ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' -= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit()'} do |f| += form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit($event)'} do |f| - if order_cycles_simple_view = render 'simple_form', f: f - else diff --git a/app/views/admin/order_cycles/new.html.haml b/app/views/admin/order_cycles/new.html.haml index 716e537874..817f790b19 100644 --- a/app/views/admin/order_cycles/new.html.haml +++ b/app/views/admin/order_cycles/new.html.haml @@ -2,7 +2,7 @@ - ng_controller = order_cycles_simple_view ? 'AdminSimpleCreateOrderCycleCtrl' : 'AdminCreateOrderCycleCtrl' -= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit()'} do |f| += form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit($event)'} do |f| - if order_cycles_simple_view = render 'simple_form', f: f - else diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 7ecd2b4782..6a1e1be70f 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -153,7 +153,8 @@ describe 'OrderCycle controllers', -> expect(OrderCycle.removeDistributionOfVariant).toHaveBeenCalledWith('variant') it 'Submits the order cycle via OrderCycle create', -> - scope.submit() + eventMock = { preventDefault: -> } + scope.submit(eventMock) expect(OrderCycle.create).toHaveBeenCalled() describe 'AdminEditOrderCycleCtrl', -> @@ -313,7 +314,8 @@ describe 'OrderCycle controllers', -> expect(OrderCycle.removeDistributionOfVariant).toHaveBeenCalledWith('variant') it 'Submits the order cycle via OrderCycle update', -> - scope.submit() + eventMock = { preventDefault: -> } + scope.submit(eventMock) expect(OrderCycle.update).toHaveBeenCalled() @@ -526,7 +528,7 @@ describe 'OrderCycle services', -> incoming: false variants: {1: true, 2: false, 3: true} enterprise_fees: [] - + describe "removing incoming exchanges", -> beforeEach -> exchange.incoming = true @@ -844,4 +846,3 @@ describe 'OrderCycle services', -> expect(order_cycle.outgoing_exchanges[0].enterprise_fees).toEqual [{id: 3}, {id: 4}] expect(order_cycle.incoming_exchanges[0].enterprise_fee_ids).toBeUndefined() expect(order_cycle.outgoing_exchanges[0].enterprise_fee_ids).toBeUndefined() - From 76acbb6159a5b2908730f6bc63294f82cbad7743 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 21 Jan 2015 16:03:27 +1100 Subject: [PATCH 567/681] Empty arrays are no longer considered falsy by angular.js parser (see https://github.com/angular/angular.js/commit/bdfc9c02d021e08babfbc966a007c71b4946d69d) --- .../_exchange_distributed_products_form.html.haml | 2 +- .../order_cycles/_exchange_supplied_products_form.html.haml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml b/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml index ae0b9b7349..6566ee770a 100644 --- a/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml @@ -8,7 +8,7 @@ .exchange-product-details .supplier {{ product.supplier_name }} %label - = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants', 'ng-disabled' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}' + = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-disabled' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}' %img{'ng-src' => '{{ product.image_url }}'} {{ product.name }} .exchange-product-variant{'ng-repeat' => 'variant in product.variants | filter:variantSuppliedToOrderCycle'} diff --git a/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml b/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml index ac791d7fd2..4e4ae4c3f3 100644 --- a/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml @@ -9,13 +9,13 @@ .exchange-product-details %label - = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants', 'ng-disabled' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'ofn-sync-distributions' => '{{ product.master_id }}', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}' + = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-disabled' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'ofn-sync-distributions' => '{{ product.master_id }}', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}' %img{'ng-src' => '{{ product.image_url }}'} {{ product.name }} -# When the master variant is in the order cycle but the product has variants, we want to -# be able to remove the master variant, since it serves no purpose. Display a checkbox to do so. - .exchange-product-variant{'ng-show' => 'exchange.variants[product.master_id] && product.variants'} + .exchange-product-variant{'ng-show' => 'exchange.variants[product.master_id] && product.variants.length > 0'} %label = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-model' => 'exchange.variants[product.master_id]', 'ofn-sync-distributions' => '{{ product.master_id }}', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}' Obsolete master From 0791cc3c2a875ded33ca5f3daa388e63d47317f9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 21 Jan 2015 16:59:32 +1100 Subject: [PATCH 568/681] Making sure that we have a .path() in HashNavigation --- .../darkswarm/services/hash_navigation.js.coffee | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/hash_navigation.js.coffee b/app/assets/javascripts/darkswarm/services/hash_navigation.js.coffee index d6fd3525ee..2cf6ad588b 100644 --- a/app/assets/javascripts/darkswarm/services/hash_navigation.js.coffee +++ b/app/assets/javascripts/darkswarm/services/hash_navigation.js.coffee @@ -1,9 +1,14 @@ Darkswarm.factory 'HashNavigation', ($location) -> new class HashNavigation - hash: null + hash: null + + constructor: -> + # Make sure we have a path as hashes + # dont seem to work so well without them + $location.path("") if !$location.path() active: (hash)-> - $location.hash() == hash + $location.hash() == hash navigate: (hash)-> @hash = hash From 6ebd58b67d22653598a731a563e30e0162034bbf Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 22 Jan 2015 09:24:23 +1100 Subject: [PATCH 569/681] Updating home spec to reflect new shopfront url --- spec/features/consumer/home_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/consumer/home_spec.rb b/spec/features/consumer/home_spec.rb index f5b8091432..128b1f734f 100644 --- a/spec/features/consumer/home_spec.rb +++ b/spec/features/consumer/home_spec.rb @@ -13,13 +13,13 @@ feature 'Home', js: true do let!(:er) { create(:enterprise_relationship, parent: distributor, child: producer) } before do - visit "/" + visit "/" end it "shows hubs" do page.should have_content distributor.name expand_active_table_node distributor.name - page.should have_content "OUR PRODUCERS" + page.should have_content "OUR PRODUCERS" end it "does not show invisible hubs" do @@ -35,7 +35,7 @@ feature 'Home', js: true do it "should link to the hub page" do follow_active_table_node distributor.name - current_path.should == "/shop" + current_path.should == enterprise_shop_path(distributor) end it "should show hub producer modals" do From b0a29801b6005bcb102b5cdc9a24d7be9ddbf3fa Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 22 Jan 2015 09:25:34 +1100 Subject: [PATCH 570/681] Making spec controller product request spec a little bit more robust --- spec/controllers/shop_controller_spec.rb | 87 +++++++++++++----------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 1a0ad25533..4b7c53da19 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -80,60 +80,65 @@ describe ShopController do end describe "returning products" do - let(:product) { create(:product) } let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator: create(:distributor_enterprise)) } let(:exchange) { Exchange.find(order_cycle.exchanges.to_enterprises(d).outgoing.first.id) } - before do - exchange.variants << product.master + describe "requests and responses" do + let(:product) { create(:product) } + before do + exchange.variants << product.master + end + + it "returns products via json" do + controller.stub(:current_order_cycle).and_return order_cycle + xhr :get, :products + response.should be_success + end + + it "does not return products if no order_cycle is selected" do + controller.stub(:current_order_cycle).and_return nil + xhr :get, :products + response.status.should == 404 + response.body.should be_empty + end end - it "returns products via json" do - controller.stub(:current_order_cycle).and_return order_cycle - xhr :get, :products - response.should be_success - end + describe "sorting" do + let(:t1) { create(:taxon) } + let(:t2) { create(:taxon) } + let!(:p1) { create(:product, name: "abc", primary_taxon_id: t2.id) } + let!(:p2) { create(:product, name: "def", primary_taxon_id: t1.id) } + let!(:p3) { create(:product, name: "ghi", primary_taxon_id: t2.id) } + let!(:p4) { create(:product, name: "jkl", primary_taxon_id: t1.id) } - it "sorts products by the distributor's preferred taxon list" do - t1 = create(:taxon) - t2 = create(:taxon) - d.stub(:preferred_shopfront_taxon_order) {"#{t1.id},#{t2.id}"} - p1 = create(:product, primary_taxon_id: t2.id, name: 'abc') - p2 = create(:product, primary_taxon_id: t1.id, name: 'def') - p3 = create(:product, primary_taxon_id: t2.id, name: 'abcd') - p4 = create(:product, primary_taxon_id: t1.id, name: 'defg') - exchange.variants << p1.master - exchange.variants << p2.master - exchange.variants << p3.master - exchange.variants << p4.master - controller.stub(:current_order_cycle).and_return order_cycle - order_cycle.stub(:valid_products_distributed_by) { Spree::Product.where( id: [p1, p2, p3, p4] ) } - xhr :get, :products - assigns[:products].should == [p2, p4, p1, p3] - end + before do + exchange.variants << p1.master + exchange.variants << p2.master + exchange.variants << p3.master + exchange.variants << p4.master + end - it "alphabetizes products by name when taxon list is not set" do - d.stub(:preferred_shopfront_taxon_order) {""} - p1 = create(:product, name: "abc") - p2 = create(:product, name: "def") - exchange.variants << p1.master - exchange.variants << p2.master - controller.stub(:current_order_cycle).and_return order_cycle - order_cycle.stub(:valid_products_distributed_by) { Spree::Product.where( id: [p1, p2] ) } - xhr :get, :products - assigns[:products].should == [p1, p2] - end + it "sorts products by the distributor's preferred taxon list" do + d.stub(:preferred_shopfront_taxon_order) {"#{t1.id},#{t2.id}"} + controller.stub(:current_order_cycle).and_return order_cycle + xhr :get, :products + assigns[:products].should == [p2, p4, p1, p3] + end - it "does not return products if no order_cycle is selected" do - controller.stub(:current_order_cycle).and_return nil - xhr :get, :products - response.status.should == 404 - response.body.should be_empty + it "alphabetizes products by name when taxon list is not set" do + d.stub(:preferred_shopfront_taxon_order) {""} + controller.stub(:current_order_cycle).and_return order_cycle + xhr :get, :products + assigns[:products].should == [p1, p2, p3, p4] + end end context "RABL tests" do render_views + let(:product) { create(:product) } + before do + exchange.variants << product.master controller.stub(:current_order_cycle).and_return order_cycle end it "only returns products for the current order cycle" do From d12fdd23fbc084cd15dcc82b9086c77b0ab05fd7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 22 Jan 2015 10:46:01 +1100 Subject: [PATCH 571/681] Fixing navcheck callback --- .../enterprises/controllers/enterprise_controller.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 147c08943d..5a8141352b 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -12,7 +12,7 @@ angular.module("admin.enterprises") # from a directive "nav-check" in the page - if we pass it here it will be called in the test suite, # and on all new uses of this contoller, and we might not want that . enterpriseNavCallback = -> - if $scope.enterprise.$dirty + if $scope.Enterprise.$dirty "Your changes to the enterprise are not saved yet." # Register the NavigationCheck callback From 4829e596634fd50ec9c08b581a5145a90c008a42 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 22 Jan 2015 10:46:31 +1100 Subject: [PATCH 572/681] Fixing checkout controller spec --- .../checkout_controller_spec.js.coffee | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee index 82b402d50b..390a7a1c94 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee @@ -3,7 +3,7 @@ describe "CheckoutCtrl", -> scope = null Checkout = null CurrentUser = null - CurrentHubMock = + CurrentHubMock = hub: id: 1 storage = null @@ -13,9 +13,9 @@ describe "CheckoutCtrl", -> angular.module('Darkswarm').value('user', {}) angular.module('Darkswarm').value('currentHub', {id: 1}) module ($provide)-> - $provide.value "CurrentHub", CurrentHubMock + $provide.value "CurrentHub", CurrentHubMock null - Checkout = + Checkout = submit: -> navigate: -> bindFieldsToLocalStorage: -> @@ -25,17 +25,18 @@ describe "CheckoutCtrl", -> user_id: 1 secrets: card_number: "this is a secret" - + describe "with user", -> beforeEach -> inject ($controller, $rootScope, _storage_) -> storage = _storage_ spyOn(storage, "bind").andCallThrough() - scope = $rootScope.$new() - ctrl = $controller 'CheckoutCtrl', {$scope: scope, Checkout: Checkout, CurrentUser: {}} + scope = $rootScope.$new() + CurrentUser = { id: 1 } + ctrl = $controller 'CheckoutCtrl', {$scope: scope, Checkout: Checkout, CurrentUser: CurrentUser } describe "submitting", -> - event = + event = preventDefault: -> beforeEach -> @@ -57,14 +58,15 @@ describe "CheckoutCtrl", -> describe "Local storage", -> it "binds to localStorage when given a scope", -> - prefix = "order_#{scope.order.id}#{CurrentUser?.id}#{CurrentHubMock.hub.id}" + prefix = "order_#{scope.order.id}#{CurrentUser.id or ""}#{CurrentHubMock.hub.id}" + console.log prefix field = scope.fieldsToBind[0] expect(storage.bind).toHaveBeenCalledWith(scope, "Checkout.order.#{field}", {storeName: "#{prefix}_#{field}"}) expect(storage.bind).toHaveBeenCalledWith(scope, "Checkout.ship_address_same_as_billing", {storeName: "#{prefix}_sameasbilling", defaultValue: true}) it "it can retrieve data from localstorage", -> - prefix = "order_#{scope.order.id}#{CurrentUser?.id}#{CurrentHubMock.hub.id}" - expect(localStorage.getItem("#{prefix}_email")).toMatch "public" + prefix = "order_#{scope.order.id}#{CurrentUser.id or ""}#{CurrentHubMock.hub.id}" + expect(localStorage.getItem("#{prefix}_email")).toMatch "public" it "does not store secrets in local storage", -> Checkout.secrets = @@ -76,8 +78,8 @@ describe "CheckoutCtrl", -> describe "without user", -> beforeEach -> inject ($controller, $rootScope) -> - scope = $rootScope.$new() - ctrl = $controller 'CheckoutCtrl', {$scope: scope, Checkout: Checkout, CurrentUser: undefined} + scope = $rootScope.$new() + ctrl = $controller 'CheckoutCtrl', {$scope: scope, Checkout: Checkout, CurrentUser: {}} it "is disabled", -> expect(scope.enabled).toEqual false From 87b092fdf774f75bbcff44bb18fb7bf3adfcd91f Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 22 Jan 2015 12:10:54 +1100 Subject: [PATCH 573/681] Adding addresses to existing groups and make them changable --- app/models/enterprise_group.rb | 3 ++- .../admin/enterprise_groups/_inputs.html.haml | 5 +++-- ...ress_instances_to_existing_enterprise_groups.rb | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 5b3b65e311..9a398e8c39 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -12,6 +12,7 @@ class EnterpriseGroup < ActiveRecord::Base attr_accessible :name, :description, :long_description, :on_front_page, :enterprise_ids attr_accessible :logo, :promo_image + attr_accessible :address_attributes attr_accessible :email, :website, :facebook, :instagram, :linkedin, :twitter delegate :phone, :to => :address @@ -37,7 +38,7 @@ class EnterpriseGroup < ActiveRecord::Base scope :on_front_page, where(on_front_page: true) def set_unused_address_fields - address.firstname = address.lastname = address.phone = 'unused' if address.present? + address.firstname = address.lastname = 'unused' if address.present? end end diff --git a/app/views/admin/enterprise_groups/_inputs.html.haml b/app/views/admin/enterprise_groups/_inputs.html.haml index fabebdaef4..8b2db582a9 100644 --- a/app/views/admin/enterprise_groups/_inputs.html.haml +++ b/app/views/admin/enterprise_groups/_inputs.html.haml @@ -52,9 +52,10 @@ %legend Contact .row .alpha.three.columns - = f.label :phone + = af.label :phone + %span.required * .omega.eight.columns - = f.text_field :phone, { placeholder: "eg. 98 7654 3210"} + = af.text_field :phone, { placeholder: "eg. 98 7654 3210"} .row .three.columns.alpha = af.label :address1 diff --git a/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb b/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb new file mode 100644 index 0000000000..2978d286dc --- /dev/null +++ b/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb @@ -0,0 +1,14 @@ +class AddAddressInstancesToExistingEnterpriseGroups < ActiveRecord::Migration + def change + country = Spree::Country.find_by_name(ENV['DEFAULT_COUNTRY']) + state = country.states.first + EnterpriseGroup.all.each do |g| + if g.address.present? then + next + end + address = Spree::Address.new(firstname: 'unused', lastname: 'unused', address1: 'undefined', city: 'undefined', zipcode: 'undefined', state: state, country: country, phone: 'undefined') + g.address = address + g.save + end + end +end From b429be707c69b39bbee65c2a7c3055df5bcdc42b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 22 Jan 2015 12:19:41 +1100 Subject: [PATCH 574/681] Split out payment and shipping methods from admin enterprises controller as services --- .../enterprise_controller.js.coffee | 38 ++---------- .../enterprise_payment_methods.js.coffee | 20 ++++++ .../enterprise_shipping_methods.js.coffee | 20 ++++++ .../services/payment_methods.js.coffee | 2 +- .../services/shipping_methods.js.coffee | 2 +- .../enterprise_controller_spec.js.coffee | 62 +------------------ .../enterprise_payment_methods_spec.js.coffee | 45 ++++++++++++++ ...enterprise_shipping_methods_spec.js.coffee | 45 ++++++++++++++ .../services/enterprise_spec.js.coffee | 8 ++- 9 files changed, 144 insertions(+), 98 deletions(-) create mode 100644 app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee create mode 100644 app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee create mode 100644 spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee create mode 100644 spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 5a8141352b..a9a6f2dd2d 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,10 +1,10 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, NavigationCheck, Enterprise, PaymentMethods, ShippingMethods, SideMenu) -> + .controller "enterpriseCtrl", ($scope, NavigationCheck, Enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu) -> $scope.Enterprise = Enterprise.enterprise - $scope.PaymentMethods = PaymentMethods.paymentMethods - $scope.ShippingMethods = ShippingMethods.shippingMethods + console.log Enterprise + $scope.PaymentMethods = EnterprisePaymentMethods.paymentMethods + $scope.ShippingMethods = EnterpriseShippingMethods.shippingMethods $scope.navClear = NavigationCheck.clear - # htmlVariable is used by textAngular wysiwyg for the long descrtiption. $scope.pristineEmail = $scope.Enterprise.email $scope.menu = SideMenu @@ -17,33 +17,3 @@ angular.module("admin.enterprises") # Register the NavigationCheck callback NavigationCheck.register(enterpriseNavCallback) - - for payment_method in $scope.PaymentMethods - payment_method.selected = payment_method.id in $scope.Enterprise.payment_method_ids - - $scope.paymentMethodsColor = -> - if $scope.PaymentMethods.length > 0 - if $scope.selectedPaymentMethodsCount() > 0 then "blue" else "red" - else - "red" - - $scope.selectedPaymentMethodsCount = -> - $scope.PaymentMethods.reduce (count, payment_method) -> - count++ if payment_method.selected - count - , 0 - - for shipping_method in $scope.ShippingMethods - shipping_method.selected = shipping_method.id in $scope.Enterprise.shipping_method_ids - - $scope.shippingMethodsColor = -> - if $scope.ShippingMethods.length > 0 - if $scope.selectedShippingMethodsCount() > 0 then "blue" else "red" - else - "red" - - $scope.selectedShippingMethodsCount = -> - $scope.ShippingMethods.reduce (count, shipping_method) -> - count++ if shipping_method.selected - count - , 0 diff --git a/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee new file mode 100644 index 0000000000..b1a88f82fb --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee @@ -0,0 +1,20 @@ +angular.module("admin.enterprises") + .factory "EnterprisePaymentMethods", (Enterprise, PaymentMethods) -> + new class EnterprisePaymentMethods + paymentMethods: PaymentMethods.paymentMethods + + constructor: -> + for payment_method in @paymentMethods + payment_method.selected = payment_method.id in Enterprise.enterprise.payment_method_ids + + displayColor: -> + if @paymentMethods.length > 0 && @selectedCount() > 0 + "blue" + else + "red" + + selectedCount: -> + @paymentMethods.reduce (count, payment_method) -> + count++ if payment_method.selected + count + , 0 diff --git a/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee new file mode 100644 index 0000000000..3f64c07442 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee @@ -0,0 +1,20 @@ +angular.module("admin.enterprises") + .factory "EnterpriseShippingMethods", (Enterprise, ShippingMethods) -> + new class EnterpriseShippingMethods + shippingMethods: ShippingMethods.shippingMethods + + constructor: -> + for shipping_method in @shippingMethods + shipping_method.selected = shipping_method.id in Enterprise.enterprise.shipping_method_ids + + displayColor: -> + if @shippingMethods.length > 0 && @selectedCount() > 0 + "blue" + else + "red" + + selectedCount: -> + @shippingMethods.reduce (count, shipping_method) -> + count++ if shipping_method.selected + count + , 0 diff --git a/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee b/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee index 53fdcb93e4..21e557cac3 100644 --- a/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee @@ -2,7 +2,7 @@ angular.module("admin.payment_methods") .factory "PaymentMethods", (paymentMethods) -> new class PaymentMethods paymentMethods: paymentMethods - + findByID: (id) -> for paymentMethod in @paymentMethods return paymentMethod if paymentMethod.id is id diff --git a/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee b/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee index eddc1aaf50..556445c869 100644 --- a/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee @@ -2,7 +2,7 @@ angular.module("admin.shipping_methods") .factory "ShippingMethods", (shippingMethods) -> new class ShippingMethods shippingMethods: shippingMethods - + findByID: (id) -> for shippingMethod in @shippingMethods return shippingMethod if shippingMethod.id is id diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee index 4018ff80bc..19fc22cb65 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee @@ -9,18 +9,16 @@ describe "enterpriseCtrl", -> module('admin.enterprises') Enterprise = enterprise: - payment_method_ids: [ 1, 3 ] - shipping_method_ids: [ 2, 4 ] is_primary_producer: true sells: "none" PaymentMethods = - paymentMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] + paymentMethods: "payment methods" ShippingMethods = - shippingMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] + shippingMethods: "shipping methods" inject ($rootScope, $controller) -> scope = $rootScope - ctrl = $controller 'enterpriseCtrl', {$scope: scope, Enterprise: Enterprise, PaymentMethods: PaymentMethods, ShippingMethods: ShippingMethods} + ctrl = $controller 'enterpriseCtrl', {$scope: scope, Enterprise: Enterprise, EnterprisePaymentMethods: PaymentMethods, EnterpriseShippingMethods: ShippingMethods} describe "initialisation", -> it "stores enterprise", -> @@ -31,57 +29,3 @@ describe "enterpriseCtrl", -> it "stores shipping methods", -> expect(scope.ShippingMethods).toBe ShippingMethods.shippingMethods - - it "sets the selected property of each payment method", -> - expect(PaymentMethods.paymentMethods[0].selected).toBe true - expect(PaymentMethods.paymentMethods[1].selected).toBe false - expect(PaymentMethods.paymentMethods[2].selected).toBe true - expect(PaymentMethods.paymentMethods[3].selected).toBe false - - it "sets the selected property of each shipping method", -> - expect(ShippingMethods.shippingMethods[0].selected).toBe false - expect(ShippingMethods.shippingMethods[1].selected).toBe true - expect(ShippingMethods.shippingMethods[2].selected).toBe false - expect(ShippingMethods.shippingMethods[3].selected).toBe true - - describe "determining payment method colour", -> - it "returns 'blue' when at least one payment method is selected", -> - scope.PaymentMethods = [ { id: 1 } ] - spyOn(scope, "selectedPaymentMethodsCount").andReturn 1 - expect(scope.paymentMethodsColor()).toBe "blue" - - it "returns 'red' when no payment methods are selected", -> - scope.PaymentMethods = [ { id: 1 } ] - spyOn(scope, "selectedPaymentMethodsCount").andReturn 0 - expect(scope.paymentMethodsColor()).toBe "red" - - it "returns 'red' when no payment methods exist", -> - scope.PaymentMethods = [ ] - spyOn(scope, "selectedPaymentMethodsCount").andReturn 1 - expect(scope.paymentMethodsColor()).toBe "red" - - describe "counting selected payment methods", -> - it "counts only payment methods with selected: true", -> - scopePaymentMethods = [ { selected: true }, { selected: false }, { selected: false }, { selected: true } ] - expect(scope.selectedPaymentMethodsCount()).toBe 2 - - describe "determining shipping method colour", -> - it "returns 'blue' when at least one shipping method is selected", -> - scope.ShippingMethods = [ { id: 1 } ] - spyOn(scope, "selectedShippingMethodsCount").andReturn 1 - expect(scope.shippingMethodsColor()).toBe "blue" - - it "returns 'red' when no shipping methods are selected", -> - scope.ShippingMethods = [ { id: 1 } ] - spyOn(scope, "selectedShippingMethodsCount").andReturn 0 - expect(scope.shippingMethodsColor()).toBe "red" - - it "returns 'red' when no shipping method exist", -> - scope.ShippingMethods = [ ] - spyOn(scope, "selectedShippingMethodsCount").andReturn 1 - expect(scope.shippingMethodsColor()).toBe "red" - - describe "counting selected shipping methods", -> - it "counts only shipping methods with selected: true", -> - scope.ShippingMethods = [ { selected: true }, { selected: true }, { selected: false }, { selected: true } ] - expect(scope.selectedShippingMethodsCount()).toBe 3 diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee new file mode 100644 index 0000000000..28052c59f9 --- /dev/null +++ b/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee @@ -0,0 +1,45 @@ +describe "EnterprisePaymentMethods service", -> + Enterprise = null + PaymentMethods = null + EnterprisePaymentMethods = null + + beforeEach -> + Enterprise = + enterprise: + payment_method_ids: [ 1, 3 ] + PaymentMethods = + paymentMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] + + module 'admin.enterprises' + module ($provide) -> + $provide.value 'PaymentMethods', PaymentMethods + $provide.value 'Enterprise', Enterprise + null + + inject (_EnterprisePaymentMethods_) -> + EnterprisePaymentMethods = _EnterprisePaymentMethods_ + + describe "selecting payment methods", -> + it "sets the selected property of each payment method", -> + expect(PaymentMethods.paymentMethods[0].selected).toBe true + expect(PaymentMethods.paymentMethods[1].selected).toBe false + expect(PaymentMethods.paymentMethods[2].selected).toBe true + expect(PaymentMethods.paymentMethods[3].selected).toBe false + + describe "determining payment method colour", -> + it "returns 'blue' when at least one payment method is selected", -> + spyOn(EnterprisePaymentMethods, "selectedCount").andReturn 1 + expect(EnterprisePaymentMethods.displayColor()).toBe "blue" + + it "returns 'red' when no payment methods are selected", -> + spyOn(EnterprisePaymentMethods, "selectedCount").andReturn 0 + expect(EnterprisePaymentMethods.displayColor()).toBe "red" + + it "returns 'red' when no payment methods exist", -> + EnterprisePaymentMethods.paymentMethods = [] + spyOn(EnterprisePaymentMethods, "selectedCount").andReturn 1 + expect(EnterprisePaymentMethods.displayColor()).toBe "red" + + describe "counting selected payment methods", -> + it "counts only payment methods with selected: true", -> + expect(EnterprisePaymentMethods.selectedCount()).toBe 2 diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee new file mode 100644 index 0000000000..4cbcf9ab25 --- /dev/null +++ b/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee @@ -0,0 +1,45 @@ +describe "EnterpriseShippingMethods service", -> + Enterprise = null + ShippingMethods = null + EnterpriseShippingMethods = null + + beforeEach -> + Enterprise = + enterprise: + shipping_method_ids: [ 1, 3 ] + ShippingMethods = + shippingMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] + + module 'admin.enterprises' + module ($provide) -> + $provide.value 'ShippingMethods', ShippingMethods + $provide.value 'Enterprise', Enterprise + null + + inject (_EnterpriseShippingMethods_) -> + EnterpriseShippingMethods = _EnterpriseShippingMethods_ + + describe "selecting shipping methods", -> + it "sets the selected property of each shipping method", -> + expect(ShippingMethods.shippingMethods[0].selected).toBe true + expect(ShippingMethods.shippingMethods[1].selected).toBe false + expect(ShippingMethods.shippingMethods[2].selected).toBe true + expect(ShippingMethods.shippingMethods[3].selected).toBe false + + describe "determining shipping method colour", -> + it "returns 'blue' when at least one shipping method is selected", -> + spyOn(EnterpriseShippingMethods, "selectedCount").andReturn 1 + expect(EnterpriseShippingMethods.displayColor()).toBe "blue" + + it "returns 'red' when no shipping methods are selected", -> + spyOn(EnterpriseShippingMethods, "selectedCount").andReturn 0 + expect(EnterpriseShippingMethods.displayColor()).toBe "red" + + it "returns 'red' when no shipping methods exist", -> + EnterpriseShippingMethods.shippingMethods = [] + spyOn(EnterpriseShippingMethods, "selectedCount").andReturn 1 + expect(EnterpriseShippingMethods.displayColor()).toBe "red" + + describe "counting selected shipping methods", -> + it "counts only shipping methods with selected: true", -> + expect(EnterpriseShippingMethods.selectedCount()).toBe 2 diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprise_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprise_spec.js.coffee index ed2b9be3c3..220d06fb9c 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprise_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprise_spec.js.coffee @@ -3,10 +3,12 @@ describe "Enterprise service", -> enterprise = { name: "test ent name" } beforeEach -> module 'admin.enterprises' - angular.module('admin.enterprises').value('enterprise', enterprise) + module ($provide) -> + $provide.value 'enterprise', enterprise + null inject ($injector) -> - Enterprise = $injector.get("Enterprise") + Enterprise = $injector.get("Enterprise") it "stores enterprise value as Enterprise.enterprise", -> - expect(Enterprise.enterprise).toBe enterprise \ No newline at end of file + expect(Enterprise.enterprise).toBe enterprise From 7a48d7fe2281b8138a990526f00fffdac1fde645 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 22 Jan 2015 16:04:46 +1100 Subject: [PATCH 575/681] Updating feature spec for groups --- spec/factories.rb | 1 + spec/features/admin/enterprise_groups_spec.rb | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index 214c61962d..d18d57ad22 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -136,6 +136,7 @@ FactoryGirl.define do name 'Enterprise group' description 'this is a group' on_front_page false + address { FactoryGirl.build(:address) } end sequence(:calculator_amount) diff --git a/spec/features/admin/enterprise_groups_spec.rb b/spec/features/admin/enterprise_groups_spec.rb index 24e89e9be7..59712f3bfe 100644 --- a/spec/features/admin/enterprise_groups_spec.rb +++ b/spec/features/admin/enterprise_groups_spec.rb @@ -15,28 +15,33 @@ feature %q{ e = create(:enterprise) group = create(:enterprise_group, enterprises: [e], on_front_page: true) - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' page.should have_selector 'td', text: group.name page.should have_selector 'td', text: 'Y' page.should have_selector 'td', text: e.name end - scenario "creating a new enterprise group" do + scenario "creating a new enterprise group", js: true do e1 = create(:enterprise) e2 = create(:enterprise) e3 = create(:enterprise) - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' 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' + select2_search e1.name, from: 'Enterprises' + select2_search e2.name, from: 'Enterprises' + click_link 'Contact' + fill_in 'enterprise_group_address_attributes_phone', with: '000' + fill_in 'enterprise_group_address_attributes_address1', with: 'My Street' + fill_in 'enterprise_group_address_attributes_city', with: 'Block' + fill_in 'enterprise_group_address_attributes_zipcode', with: '0000' + select2_search 'Australia', :from => 'Country' + select2_search 'Victoria', :from => 'State' click_button 'Create' page.should have_content 'Enterprise group "EGEGEG" has been successfully created!' @@ -53,8 +58,7 @@ feature %q{ e2 = create(:enterprise) eg = create(:enterprise_group, name: 'EGEGEG', on_front_page: true, enterprises: [e1, e2]) - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' first("a.edit-enterprise-group").click page.should have_field 'enterprise_group_name', with: 'EGEGEG' @@ -80,8 +84,7 @@ feature %q{ eg1 = create(:enterprise_group, name: 'A') eg2 = create(:enterprise_group, name: 'B') - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' page.all('td.name').map(&:text).should == ['A', 'B'] all("a.move-down").first.click @@ -93,8 +96,7 @@ feature %q{ scenario "deleting an enterprise group", js: true do eg = create(:enterprise_group, name: 'EGEGEG') - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' first("a.delete-resource").click page.should have_no_content 'EGEGEG' From d93a8b642870d0ba2273a0fe93fc226fcac893e7 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 22 Jan 2015 16:22:34 +1100 Subject: [PATCH 576/681] Enterprise and Group controllers use default country instead of default country id --- app/controllers/admin/enterprise_groups_controller.rb | 2 +- app/controllers/admin/enterprises_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/enterprise_groups_controller.rb b/app/controllers/admin/enterprise_groups_controller.rb index 7a0763f5a4..89d72262d5 100644 --- a/app/controllers/admin/enterprise_groups_controller.rb +++ b/app/controllers/admin/enterprise_groups_controller.rb @@ -22,7 +22,7 @@ module Admin def build_resource_with_address enterprise_group = build_resource_without_address enterprise_group.address = Spree::Address.new - enterprise_group.address.country = Spree::Country.find_by_id(Spree::Config[:default_country_id]) + enterprise_group.address.country = Spree::Country.find_by_name(ENV['DEFAULT_COUNTRY']) enterprise_group end alias_method_chain :build_resource, :address diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 0ee0541145..150c609d51 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -59,7 +59,7 @@ module Admin def build_resource_with_address enterprise = build_resource_without_address enterprise.address = Spree::Address.new - enterprise.address.country = Spree::Country.find_by_id(Spree::Config[:default_country_id]) + enterprise.address.country = Spree::Country.find_by_name(ENV['DEFAULT_COUNTRY']) enterprise end alias_method_chain :build_resource, :address From 8e8f38e4848d289187c19d57aaac5c0ba9681030 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Sat, 27 Dec 2014 15:38:19 +0000 Subject: [PATCH 577/681] app/controllers/spree/admin/reports_controller_decorator.rb --- app/controllers/spree/admin/reports_controller_decorator.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 1c42ccdbf3..e00b26203b 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -58,7 +58,6 @@ Spree::Admin::ReportsController.class_eval do @report_types = REPORT_TYPES[:customers] @report_type = params[:report_type] @report = OpenFoodNetwork::CustomersReport.new spree_current_user, params - render_report(@report.header, @report.table, params[:csv], "customers_#{timestamp}.csv") end @@ -252,7 +251,6 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(@line_items) csv_file_name = "bulk_coop_#{timestamp}.csv" - render_report(@header, @table, params[:csv], csv_file_name) end @@ -356,7 +354,6 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(table_items) csv_file_name = "payments_#{timestamp}.csv" - render_report(@header, @table, params[:csv], csv_file_name) end @@ -588,7 +585,6 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(table_items) csv_file_name = "#{params[:report_type]}_#{timestamp}.csv" - render_report(@header, @table, params[:csv], csv_file_name) end From 3c4ad4121fd7a02b8206c4646d7dbb3e78cd1827 Mon Sep 17 00:00:00 2001 From: Lynne Davis Date: Thu, 22 Jan 2015 14:00:48 +0000 Subject: [PATCH 578/681] LD adding whitespace to get my master version back in line with origin --- app/controllers/spree/admin/reports_controller_decorator.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index e00b26203b..655d1778dc 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -58,6 +58,7 @@ Spree::Admin::ReportsController.class_eval do @report_types = REPORT_TYPES[:customers] @report_type = params[:report_type] @report = OpenFoodNetwork::CustomersReport.new spree_current_user, params + render_report(@report.header, @report.table, params[:csv], "customers_#{timestamp}.csv") end @@ -251,6 +252,7 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(@line_items) csv_file_name = "bulk_coop_#{timestamp}.csv" + render_report(@header, @table, params[:csv], csv_file_name) end @@ -354,6 +356,7 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(table_items) csv_file_name = "payments_#{timestamp}.csv" + render_report(@header, @table, params[:csv], csv_file_name) end @@ -585,6 +588,7 @@ Spree::Admin::ReportsController.class_eval do @header = header @table = order_grouper.table(table_items) csv_file_name = "#{params[:report_type]}_#{timestamp}.csv" + render_report(@header, @table, params[:csv], csv_file_name) end From adbe127e763eb4c1a1cd475b9f19273b64dcc02f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 25 Jan 2015 14:08:29 +1100 Subject: [PATCH 579/681] Revert "Update bindonce" This reverts commit 48dc85cfc2af70265131fed3aa7cb157ddf0d17e. --- app/assets/javascripts/shared/bindonce.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/shared/bindonce.min.js b/app/assets/javascripts/shared/bindonce.min.js index 8c118f69e3..0edd3d57d4 100644 --- a/app/assets/javascripts/shared/bindonce.min.js +++ b/app/assets/javascripts/shared/bindonce.min.js @@ -1 +1 @@ -!function(){"use strict";var e=angular.module("pasvaz.bindonce",[]);e.directive("bindonce",function(){var e=function(e){if(e&&0!==e.length){var t=angular.lowercase(""+e);e=!("f"===t||"0"===t||"false"===t||"no"===t||"n"===t||"[]"===t)}else e=!1;return e},t=parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10);isNaN(t)&&(t=parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10));var r={restrict:"AM",controller:["$scope","$element","$attrs","$interpolate",function(r,a,i,n){var c=function(t,r,a){var i="show"===r?"":"none",n="hide"===r?"":"none";t.css("display",e(a)?i:n)},o=function(e,t){if(angular.isObject(t)&&!angular.isArray(t)){var r=[];angular.forEach(t,function(e,t){e&&r.push(t)}),t=r}t&&e.addClass(angular.isArray(t)?t.join(" "):t)},s=function(e,t){e.transclude(t,function(t){var r=e.element.parent(),a=e.element&&e.element[e.element.length-1],i=r&&r[0]||a&&a.parentNode,n=a&&a.nextSibling||null;angular.forEach(t,function(e){i.insertBefore(e,n)})})},l={watcherRemover:void 0,binders:[],group:i.boName,element:a,ran:!1,addBinder:function(e){this.binders.push(e),this.ran&&this.runBinders()},setupWatcher:function(e){var t=this;this.watcherRemover=r.$watch(e,function(e){void 0!==e&&(t.removeWatcher(),t.checkBindonce(e))},!0)},checkBindonce:function(e){var t=this,r=e.$promise?e.$promise.then:e.then;"function"==typeof r?r(function(){t.runBinders()}):t.runBinders()},removeWatcher:function(){void 0!==this.watcherRemover&&(this.watcherRemover(),this.watcherRemover=void 0)},runBinders:function(){for(;this.binders.length>0;){var r=this.binders.shift();if(!this.group||this.group==r.group){var a=r.scope.$eval(r.interpolate?n(r.value):r.value);switch(r.attr){case"boIf":e(a)&&s(r,r.scope.$new());break;case"boSwitch":var i,l=r.controller[0];(i=l.cases["!"+a]||l.cases["?"])&&(r.scope.$eval(r.attrs.change),angular.forEach(i,function(e){s(e,r.scope.$new())}));break;case"boSwitchWhen":var u=r.controller[0];u.cases["!"+r.attrs.boSwitchWhen]=u.cases["!"+r.attrs.boSwitchWhen]||[],u.cases["!"+r.attrs.boSwitchWhen].push({transclude:r.transclude,element:r.element});break;case"boSwitchDefault":var u=r.controller[0];u.cases["?"]=u.cases["?"]||[],u.cases["?"].push({transclude:r.transclude,element:r.element});break;case"hide":case"show":c(r.element,r.attr,a);break;case"class":o(r.element,a);break;case"text":r.element.text(a);break;case"html":r.element.html(a);break;case"style":r.element.css(a);break;case"disabled":r.element.prop("disabled",a);break;case"src":r.element.attr(r.attr,a),t&&r.element.prop("src",a);break;case"attr":angular.forEach(r.attrs,function(e,t){var a,i;t.match(/^boAttr./)&&r.attrs[t]&&(a=t.replace(/^boAttr/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=r.scope.$eval(r.attrs[t]),r.element.attr(a,i))});break;case"href":case"alt":case"title":case"id":case"value":r.element.attr(r.attr,a)}}}this.ran=!0}};angular.extend(this,l)}],link:function(e,t,r,a){var i=r.bindonce&&e.$eval(r.bindonce);void 0!==i?a.checkBindonce(i):(a.setupWatcher(r.bindonce),t.bind("$destroy",a.removeWatcher))}};return r}),angular.forEach([{directiveName:"boShow",attribute:"show"},{directiveName:"boHide",attribute:"hide"},{directiveName:"boClass",attribute:"class"},{directiveName:"boText",attribute:"text"},{directiveName:"boBind",attribute:"text"},{directiveName:"boHtml",attribute:"html"},{directiveName:"boSrcI",attribute:"src",interpolate:!0},{directiveName:"boSrc",attribute:"src"},{directiveName:"boHrefI",attribute:"href",interpolate:!0},{directiveName:"boHref",attribute:"href"},{directiveName:"boAlt",attribute:"alt"},{directiveName:"boTitle",attribute:"title"},{directiveName:"boId",attribute:"id"},{directiveName:"boStyle",attribute:"style"},{directiveName:"boDisabled",attribute:"disabled"},{directiveName:"boValue",attribute:"value"},{directiveName:"boAttr",attribute:"attr"},{directiveName:"boIf",transclude:"element",terminal:!0,priority:1e3},{directiveName:"boSwitch",require:"boSwitch",controller:function(){this.cases={}}},{directiveName:"boSwitchWhen",transclude:"element",priority:800,require:"^boSwitch"},{directiveName:"boSwitchDefault",transclude:"element",priority:800,require:"^boSwitch"}],function(t){var r=200;return e.directive(t.directiveName,function(){var e={priority:t.priority||r,transclude:t.transclude||!1,terminal:t.terminal||!1,require:["^bindonce"].concat(t.require||[]),controller:t.controller,compile:function(e,r,a){return function(e,r,i,n){var c=n[0],o=i.boParent;if(o&&c.group!==o){var s=c.element.parent();c=void 0;for(var l;9!==s[0].nodeType&&s.length;){if((l=s.data("$bindonceController"))&&l.group===o){c=l;break}s=s.parent()}if(!c)throw new Error("No bindonce controller: "+o)}c.addBinder({element:r,attr:t.attribute||t.directiveName,attrs:i,value:i[t.directiveName],interpolate:t.interpolate,group:o,transclude:a,controller:n.slice(1),scope:e})}}};return e})})}(); +!function(){"use strict";var e=angular.module("pasvaz.bindonce",[]);e.directive("bindonce",function(){var e=function(e){if(e&&0!==e.length){var t=angular.lowercase(""+e);e=!("f"===t||"0"===t||"false"===t||"no"===t||"n"===t||"[]"===t)}else e=!1;return e},t=parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10);isNaN(t)&&(t=parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10));var r={restrict:"AM",controller:["$scope","$element","$attrs","$interpolate",function(r,a,i,n){var c=function(t,r,a){var i="show"===r?"":"none",n="hide"===r?"":"none";t.css("display",e(a)?i:n)},o=function(e,t){if(angular.isObject(t)&&!angular.isArray(t)){var r=[];angular.forEach(t,function(e,t){e&&r.push(t)}),t=r}t&&e.addClass(angular.isArray(t)?t.join(" "):t)},s=function(e,t){e.transclude(t,function(t){var r=e.element.parent(),a=e.element&&e.element[e.element.length-1],i=r&&r[0]||a&&a.parentNode,n=a&&a.nextSibling||null;angular.forEach(t,function(e){i.insertBefore(e,n)})})},l={watcherRemover:void 0,binders:[],group:i.boName,element:a,ran:!1,addBinder:function(e){this.binders.push(e),this.ran&&this.runBinders()},setupWatcher:function(e){var t=this;this.watcherRemover=r.$watch(e,function(e){void 0!==e&&(t.removeWatcher(),t.checkBindonce(e))},!0)},checkBindonce:function(e){var t=this,r=e.$promise?e.$promise.then:e.then;"function"==typeof r?r(function(){t.runBinders()}):t.runBinders()},removeWatcher:function(){void 0!==this.watcherRemover&&(this.watcherRemover(),this.watcherRemover=void 0)},runBinders:function(){for(;this.binders.length>0;){var r=this.binders.shift();if(!this.group||this.group==r.group){var a=r.scope.$eval(r.interpolate?n(r.value):r.value);switch(r.attr){case"boIf":e(a)&&s(r,r.scope.$new());break;case"boSwitch":var i,l=r.controller[0];(i=l.cases["!"+a]||l.cases["?"])&&(r.scope.$eval(r.attrs.change),angular.forEach(i,function(e){s(e,r.scope.$new())}));break;case"boSwitchWhen":var u=r.controller[0];u.cases["!"+r.attrs.boSwitchWhen]=u.cases["!"+r.attrs.boSwitchWhen]||[],u.cases["!"+r.attrs.boSwitchWhen].push({transclude:r.transclude,element:r.element});break;case"boSwitchDefault":var u=r.controller[0];u.cases["?"]=u.cases["?"]||[],u.cases["?"].push({transclude:r.transclude,element:r.element});break;case"hide":case"show":c(r.element,r.attr,a);break;case"class":o(r.element,a);break;case"text":r.element.text(a);break;case"html":r.element.html(a);break;case"style":r.element.css(a);break;case"src":r.element.attr(r.attr,a),t&&r.element.prop("src",a);break;case"attr":angular.forEach(r.attrs,function(e,t){var a,i;t.match(/^boAttr./)&&r.attrs[t]&&(a=t.replace(/^boAttr/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=r.scope.$eval(r.attrs[t]),r.element.attr(a,i))});break;case"href":case"alt":case"title":case"id":case"value":r.element.attr(r.attr,a)}}}this.ran=!0}};return l}],link:function(e,t,r,a){var i=r.bindonce&&e.$eval(r.bindonce);void 0!==i?a.checkBindonce(i):(a.setupWatcher(r.bindonce),t.bind("$destroy",a.removeWatcher))}};return r}),angular.forEach([{directiveName:"boShow",attribute:"show"},{directiveName:"boHide",attribute:"hide"},{directiveName:"boClass",attribute:"class"},{directiveName:"boText",attribute:"text"},{directiveName:"boBind",attribute:"text"},{directiveName:"boHtml",attribute:"html"},{directiveName:"boSrcI",attribute:"src",interpolate:!0},{directiveName:"boSrc",attribute:"src"},{directiveName:"boHrefI",attribute:"href",interpolate:!0},{directiveName:"boHref",attribute:"href"},{directiveName:"boAlt",attribute:"alt"},{directiveName:"boTitle",attribute:"title"},{directiveName:"boId",attribute:"id"},{directiveName:"boStyle",attribute:"style"},{directiveName:"boValue",attribute:"value"},{directiveName:"boAttr",attribute:"attr"},{directiveName:"boIf",transclude:"element",terminal:!0,priority:1e3},{directiveName:"boSwitch",require:"boSwitch",controller:function(){this.cases={}}},{directiveName:"boSwitchWhen",transclude:"element",priority:800,require:"^boSwitch"},{directiveName:"boSwitchDefault",transclude:"element",priority:800,require:"^boSwitch"}],function(t){var r=200;return e.directive(t.directiveName,function(){var e={priority:t.priority||r,transclude:t.transclude||!1,terminal:t.terminal||!1,require:["^bindonce"].concat(t.require||[]),controller:t.controller,compile:function(e,r,a){return function(e,r,i,n){var c=n[0],o=i.boParent;if(o&&c.group!==o){var s=c.element.parent();c=void 0;for(var l;9!==s[0].nodeType&&s.length;){if((l=s.data("$bindonceController"))&&l.group===o){c=l;break}s=s.parent()}if(!c)throw new Error("No bindonce controller: "+o)}c.addBinder({element:r,attr:t.attribute||t.directiveName,attrs:i,value:i[t.directiveName],interpolate:t.interpolate,group:o,transclude:a,controller:n.slice(1),scope:e})}}};return e})})}(); \ No newline at end of file From 21db56ce6cb293a9e102e439621f3754eeeddcd5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 28 Jan 2015 08:41:14 +1100 Subject: [PATCH 580/681] Rollback angularjs upgrade --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index d0c4df20b6..0a84a770c8 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem 'comfortable_mexican_sofa' gem 'simple_form', :github => 'RohanM/simple_form' gem 'unicorn' -gem 'angularjs-rails' +gem 'angularjs-rails', '1.2.13' gem 'bugsnag' gem 'newrelic_rpm' gem 'haml' diff --git a/Gemfile.lock b/Gemfile.lock index 416faab0d4..b365ba2952 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -154,7 +154,7 @@ GEM sprockets tilt angularjs-file-upload-rails (1.1.0) - angularjs-rails (1.3.9) + angularjs-rails (1.2.13) ansi (1.4.2) arel (3.0.3) awesome_nested_set (2.1.5) @@ -532,7 +532,7 @@ DEPENDENCIES andand angular-rails-templates angularjs-file-upload-rails (~> 1.1.0) - angularjs-rails + angularjs-rails (= 1.2.13) awesome_print aws-sdk bugsnag From 9163b0c1ad3cb815cb07cd37091f4d3f71baa8d0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 28 Jan 2015 11:37:02 +1100 Subject: [PATCH 581/681] Adding missing equals sign to test for equality in permalink migration --- db/migrate/20141219034321_add_permalink_to_enterprises.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20141219034321_add_permalink_to_enterprises.rb b/db/migrate/20141219034321_add_permalink_to_enterprises.rb index 3fe1243065..55d8553bf8 100644 --- a/db/migrate/20141219034321_add_permalink_to_enterprises.rb +++ b/db/migrate/20141219034321_add_permalink_to_enterprises.rb @@ -5,7 +5,7 @@ class AddPermalinkToEnterprises < ActiveRecord::Migration Enterprise.all.each do |enterprise| counter = 1 permalink = enterprise.name.parameterize - permalink = "my-enterprise-name" if permalink = "" + permalink = "my-enterprise-name" if permalink == "" while Enterprise.find_by_permalink(permalink) do permalink = enterprise.name.parameterize + counter.to_s counter += 1 From cbae7dcc8e3f9a9ff47d28b222c0fb627a0ebfb7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 29 Jan 2015 10:46:44 +1100 Subject: [PATCH 582/681] Adding visible to user and enterprises report, and sorting by confirmation date --- .../users_and_enterprises_report.rb | 21 ++++++++++------ ...b => users_and_enterprises_report_spec.rb} | 24 +++++++++++++++---- 2 files changed, 34 insertions(+), 11 deletions(-) rename spec/lib/open_food_network/{users_and_enterprises_report.rb => users_and_enterprises_report_spec.rb} (86%) diff --git a/lib/open_food_network/users_and_enterprises_report.rb b/lib/open_food_network/users_and_enterprises_report.rb index a501c505f0..1c078c5b2a 100644 --- a/lib/open_food_network/users_and_enterprises_report.rb +++ b/lib/open_food_network/users_and_enterprises_report.rb @@ -16,6 +16,7 @@ module OpenFoodNetwork "Enterprise", "Producer?", "Sells", + "Visible", "Confirmation Date" ] end @@ -27,25 +28,26 @@ module OpenFoodNetwork uae["name"], to_bool(uae["is_primary_producer"]), uae["sells"], + uae["visible"], to_local_datetime(uae["confirmed_at"]) ] end end def owners_and_enterprises - query = "SELECT enterprises.name, enterprises.sells, enterprises.is_primary_producer, enterprises.confirmed_at, + query = "SELECT enterprises.name, enterprises.sells, enterprises.visible, enterprises.is_primary_producer, enterprises.confirmed_at, 'owns' AS relationship_type, owners.email as user_email FROM enterprises LEFT JOIN spree_users AS owners ON owners.id=enterprises.owner_id WHERE enterprises.id IS NOT NULL #{ params[:enterprise_id_in].present? ? "AND enterprises.id IN (#{ params[:enterprise_id_in] })" : "" } #{ params[:user_id_in].present? ? "AND owners.id IN (#{ params[:user_id_in] })" : "" } - ORDER BY enterprises.name DESC" + ORDER BY confirmed_at DESC" ActiveRecord::Base.connection.execute(query).to_a end def managers_and_enterprises - query = "SELECT enterprises.name, enterprises.sells, enterprises.is_primary_producer, enterprises.confirmed_at, + query = "SELECT enterprises.name, enterprises.sells, enterprises.visible, enterprises.is_primary_producer, enterprises.confirmed_at, 'manages' AS relationship_type, managers.email as user_email FROM enterprises LEFT JOIN enterprise_roles ON enterprises.id=enterprise_roles.enterprise_id LEFT JOIN spree_users AS managers ON enterprise_roles.user_id=managers.id @@ -53,7 +55,7 @@ module OpenFoodNetwork #{ params[:enterprise_id_in].present? ? "AND enterprise_id IN (#{ params[:enterprise_id_in] })" : "" } AND user_id IS NOT NULL #{ params[:user_id_in].present? ? "AND user_id IN (#{ params[:user_id_in] })" : "" } - ORDER BY enterprises.name DESC, user_email DESC" + ORDER BY confirmed_at DESC" ActiveRecord::Base.connection.execute(query).to_a end @@ -64,8 +66,13 @@ module OpenFoodNetwork def sort(results) results.sort do |a,b| - [ a["name"], b["relationship_type"], a["user_email"] ] <=> - [ b["name"], a["relationship_type"], b["user_email"] ] + if a["confirmed_at"].nil? || b["confirmed_at"].nil? + [ (a["confirmed_at"].nil? ? 0 : 1), a["name"], b["relationship_type"], a["user_email"] ] <=> + [ (b["confirmed_at"].nil? ? 0 : 1), b["name"], a["relationship_type"], b["user_email"] ] + else + [ DateTime.parse(b["confirmed_at"]), a["name"], b["relationship_type"], a["user_email"] ] <=> + [ DateTime.parse(a["confirmed_at"]), b["name"], a["relationship_type"], b["user_email"] ] + end end end @@ -78,4 +85,4 @@ module OpenFoodNetwork string.to_datetime.in_time_zone.strftime "%Y-%m-%d %H:%M" end end -end \ No newline at end of file +end diff --git a/spec/lib/open_food_network/users_and_enterprises_report.rb b/spec/lib/open_food_network/users_and_enterprises_report_spec.rb similarity index 86% rename from spec/lib/open_food_network/users_and_enterprises_report.rb rename to spec/lib/open_food_network/users_and_enterprises_report_spec.rb index cf1676492f..7119d16239 100644 --- a/spec/lib/open_food_network/users_and_enterprises_report.rb +++ b/spec/lib/open_food_network/users_and_enterprises_report_spec.rb @@ -26,7 +26,23 @@ module OpenFoodNetwork describe "sorting results" do let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new {} } - it "sorts by name first" do + it "sorts unconfirmed enterprises to the top" do + uae_mock = [ + { "confirmed_at" => "2015-01-01", "name" => "aaa" }, + { "confirmed_at" => nil, "name" => "bbb" } + ] + expect(subject.sort uae_mock).to eq [ uae_mock[1], uae_mock[0] ] + end + + it "then sorts by confirmation date" do + uae_mock = [ + { "confirmed_at" => "2015-01-01", "name" => "bbb" }, + { "confirmed_at" => "2015-01-02", "name" => "aaa" } + ] + expect(subject.sort uae_mock).to eq [ uae_mock[1], uae_mock[0] ] + end + + it "then sorts by name" do uae_mock = [ { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "bbb" }, { "name" => "bbb", "relationship_type" => "aaa", "user_email" => "aaa" } @@ -34,7 +50,7 @@ module OpenFoodNetwork expect(subject.sort uae_mock).to eq [ uae_mock[0], uae_mock[1] ] end - it "sorts by relationship type (reveresed) second" do + it "then sorts by relationship type (reveresed)" do uae_mock = [ { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "bbb" }, { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "aaa" }, @@ -43,7 +59,7 @@ module OpenFoodNetwork expect(subject.sort uae_mock).to eq [ uae_mock[2], uae_mock[0], uae_mock[1] ] end - it "sorts by user_email third" do + it "then sorts by user_email" do uae_mock = [ { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "aaa" }, { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "aaa" }, @@ -113,4 +129,4 @@ module OpenFoodNetwork end end end -end \ No newline at end of file +end From 7a5c56cbcabcd78901a364849e9ff945b32916f6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 29 Jan 2015 13:19:04 +1100 Subject: [PATCH 583/681] Do not show out of stock lines on order confirmation page --- app/helpers/spree/base_helper.rb | 9 +++++++++ .../consumer/shopping/variant_overrides_spec.rb | 12 ++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 app/helpers/spree/base_helper.rb diff --git a/app/helpers/spree/base_helper.rb b/app/helpers/spree/base_helper.rb new file mode 100644 index 0000000000..fa058f9518 --- /dev/null +++ b/app/helpers/spree/base_helper.rb @@ -0,0 +1,9 @@ +module Spree + module BaseHelper + # human readable list of variant options + # Override: Do not show out of stock text + def variant_options(v, options={}) + v.options_text + end + end +end diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index d7bd53784a..4178aeb2cc 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -144,6 +144,18 @@ feature "shopping with variant overrides defined", js: true do end.to change { v1.reload.count_on_hand }.by(-2) vo1.reload.count_on_hand.should be_nil end + + it "does not show out of stock flags on order confirmation page" do + v4.update_attribute :count_on_hand, 0 + fill_in "variants[#{v4.id}]", with: "2" + show_cart + wait_until_enabled 'li.cart a.button' + click_link 'Checkout now' + + complete_checkout + + page.should_not have_content "Out of Stock" + end end From a25bf321566063231a6e293fd895e9b1284ae520 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 29 Jan 2015 15:15:28 +1100 Subject: [PATCH 584/681] Show extended variant name/unit info for admin variant overrides --- app/assets/stylesheets/admin/variant_overrides.css.sass | 3 +++ app/serializers/spree/api/variant_serializer.rb | 2 +- .../admin/variant_overrides/_products_variants.html.haml | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 app/assets/stylesheets/admin/variant_overrides.css.sass diff --git a/app/assets/stylesheets/admin/variant_overrides.css.sass b/app/assets/stylesheets/admin/variant_overrides.css.sass new file mode 100644 index 0000000000..c0f51658b8 --- /dev/null +++ b/app/assets/stylesheets/admin/variant_overrides.css.sass @@ -0,0 +1,3 @@ +.variant-override-unit + float: right + font-style: italic diff --git a/app/serializers/spree/api/variant_serializer.rb b/app/serializers/spree/api/variant_serializer.rb index cd31196ca0..a2342de6e4 100644 --- a/app/serializers/spree/api/variant_serializer.rb +++ b/app/serializers/spree/api/variant_serializer.rb @@ -1,5 +1,5 @@ class Spree::Api::VariantSerializer < ActiveModel::Serializer - attributes :id, :options_text, :unit_value, :unit_description, :on_demand, :display_as, :display_name + attributes :id, :options_text, :unit_value, :unit_description, :unit_to_display, :on_demand, :display_as, :display_name, :name_to_display attributes :on_hand, :price def on_hand diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index 8af837901e..bd48ea343c 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -1,6 +1,8 @@ %tr.variant{ng: {repeat: 'variant in product.variants'}} %td - %td {{ variant.options_text }} + %td + {{ variant.display_name }} + .variant-override-unit {{ variant.unit_to_display }} %td %input{name: 'variant-overrides-{{ variant.id }}-price', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].price'}, placeholder: '{{ variant.price }}', 'ofn-track-variant-override' => 'price'} From 8e42f29bdebba06dedeeb811df9bb9d91c8881e2 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 29 Jan 2015 15:52:20 +1100 Subject: [PATCH 585/681] WIP groups page styling and markup updates --- app/views/groups/show.html.haml | 329 +++++++++++++++++--------------- 1 file changed, 176 insertions(+), 153 deletions(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index c354dd36a9..64960581b9 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,159 +1,182 @@ -%div{style: "padding: 1.5em;"} - %img{"src" => @group.promo_image, style: "display: block"} - %div{style: "margin-bottom: 1em"} - %img{"src" => @group.logo} - %div{style: "display: inline-block; vertical-align: middle"} - %h2{style: "margin-bottom: 0em"}= @group.name - %div= @group.description +.row.pad-top + .small-12.columns.pad-top + %header + .row + .small-12.columns + %img{"src" => @group.promo_image} + .row.pad-top + .small-3.medium-2.columns + %img{"src" => @group.logo} + .small-9.medium-10.columns + %h2= @group.name + %p= @group.description - #div{"ng-controller" => "TabsCtrl", style: "clear: both"} - %tabset - %tab{heading: 'Map', - active: "active(\'\')", - select: "select(\'\')"} - = inject_json_ams "enterprises", @group.enterprises, Api::EnterpriseSerializer - .map-container - %map{"ng-controller" => "MapCtrl", "ng-if" => "(active(\'\') && (mapShowed = true)) || mapShowed"} - %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} - %markers{models: "OfnMap.enterprises", fit: "true", - coords: "'self'", icon: "'icon'", click: "'reveal'"} - - %tab{heading: 'About us', - active: "active(\'about\')", - select: "select(\'about\')"} - %h3 About us - %p= @group.long_description - - %tab{heading: 'Our producers', - active: "active(\'producers\')", - select: "select(\'producers\')"} - .producers.pad-top{"ng-controller" => "GroupEnterprisesCtrl"} - .row - .small-12.columns.pad-top - %h1 Our Producers - = render partial: "shared/components/enterprise_search" - -# TODO: find out why this is not working - -#= render partial: "producers/filters" - - .row{bindonce: true} - .small-12.columns - .active_table - %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", - "ng-repeat" => "producer in filteredEnterprises = (Enterprises.producers | visible | searchEnterprises:query | taxons:activeTaxons)", - "ng-controller" => "GroupEnterpriseNodeCtrl", - "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !producer.active}", - id: "{{producer.hash}}"} - - .small-12.columns - = render partial: 'producers/skinny' - = render partial: 'producers/fat' - - = render partial: 'shared/components/enterprise_no_results' - - %tab{heading: 'Our hubs', - active: "active(\'hubs\')", - select: "select(\'hubs\')"} - #hubs.hubs{"ng-controller" => "GroupEnterprisesCtrl"} - .row - .small-12.columns - %h1 Our Hubs - - = render partial: "shared/components/enterprise_search" - -# TODO: find out why this is not working - -#= render partial: "home/filters" - - .row{bindonce: true} - .small-12.columns - .active_table - %hub.active_table_node.row.animate-repeat{id: "{{hub.hash}}", - "ng-repeat" => "hub in filteredEnterprises = (Enterprises.hubs | visible | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])", - "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", - "ng-controller" => "GroupEnterpriseNodeCtrl"} - .small-12.columns - = render partial: 'home/skinny' - = render partial: 'home/fat' - - = render partial: 'shared/components/enterprise_no_results' - - %tab{heading: 'Contact us', - active: "active(\'contact\')", - select: "select(\'contact\')"} - .row - .small-6.columns - %h3 Contact us - - if @group.phone - .row - .small-2.columns - Call - .small-10.columns - = @group.phone - - if @group.email - .row - .small-2.columns - Email - .small-10.columns - = @group.email - - if @group.website - .row - .small-2.columns - Website - .small-10.columns - = @group.website - .small-6.columns - %h3 Follow us - - if @group.facebook - .row - .small-2.columns - Facebook - .small-10.columns - = @group.facebook - - if @group.instagram - .row - .small-2.columns - Instagram - .small-10.columns - = @group.instagram - - if @group.linkedin - .row - .small-2.columns - LinkedIn - .small-10.columns - = @group.linkedin - - if @group.twitter - .row - .small-2.columns - Twitter - .small-10.columns - = @group.twitter - .row - .small-6.columns - %h3 Address - %p - = @group.address.address1 - - if @group.address.address2 - %br - = @group.address.address2 - %br - = @group.address.city - , - = @group.address.state - = @group.address.zipcode - %br - = @group.address.country - - .text-center +.row.pad-top + .small-12.columns.pad-top %p - = @group.name + 123 %p - -if @group.facebook - %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} - %i.ofn-i_044-facebook - -if @group.email - %a{title:'Email us', href: @group.email.reverse, mailto: true} - %i.ofn-i_050-mail-circle - -if @group.website - %a{title:'Visit our website', href: 'http://' + @group.website, target: '_blank'} - %i.ofn-i_049-web + 456 + %p + 789 + +/ %div{style: "padding: 1.5em;"} +/ %img{"src" => @group.promo_image, style: "display: block"} +/ %div{style: "margin-bottom: 1em"} +/ %img{"src" => @group.logo} +/ %div{style: "display: inline-block; vertical-align: middle"} +/ %h2{style: "margin-bottom: 0em"}= @group.name +/ %div= @group.description + +/ #div{"ng-controller" => "TabsCtrl", style: "clear: both"} +/ %tabset + +/ %tab{heading: 'Map', +/ active: "active(\'\')", +/ select: "select(\'\')"} +/ = inject_json_ams "enterprises", @group.enterprises, Api::EnterpriseSerializer +/ .map-container +/ %map{"ng-controller" => "MapCtrl", "ng-if" => "(active(\'\') && (mapShowed = true)) || mapShowed"} +/ %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} +/ %markers{models: "OfnMap.enterprises", fit: "true", +/ coords: "'self'", icon: "'icon'", click: "'reveal'"} + +/ %tab{heading: 'About us', +/ active: "active(\'about\')", +/ select: "select(\'about\')"} +/ %h3 About us +/ %p= @group.long_description + +/ %tab{heading: 'Our producers', +/ active: "active(\'producers\')", +/ select: "select(\'producers\')"} +/ .producers.pad-top{"ng-controller" => "GroupEnterprisesCtrl"} +/ .row +/ .small-12.columns.pad-top +/ %h1 Our Producers +/ = render partial: "shared/components/enterprise_search" +/ -# TODO: find out why this is not working +/ -#= render partial: "producers/filters" + +/ .row{bindonce: true} +/ .small-12.columns +/ .active_table +/ %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", +/ "ng-repeat" => "producer in filteredEnterprises = (Enterprises.producers | visible | searchEnterprises:query | taxons:activeTaxons)", +/ "ng-controller" => "GroupEnterpriseNodeCtrl", +/ "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !producer.active}", +/ id: "{{producer.hash}}"} + +/ .small-12.columns +/ = render partial: 'producers/skinny' +/ = render partial: 'producers/fat' + +/ = render partial: 'shared/components/enterprise_no_results' + +/ %tab{heading: 'Our hubs', +/ active: "active(\'hubs\')", +/ select: "select(\'hubs\')"} +/ #hubs.hubs{"ng-controller" => "GroupEnterprisesCtrl"} +/ .row +/ .small-12.columns +/ %h1 Our Hubs + +/ = render partial: "shared/components/enterprise_search" +/ -# TODO: find out why this is not working +/ -#= render partial: "home/filters" + +/ .row{bindonce: true} +/ .small-12.columns +/ .active_table +/ %hub.active_table_node.row.animate-repeat{id: "{{hub.hash}}", +/ "ng-repeat" => "hub in filteredEnterprises = (Enterprises.hubs | visible | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])", +/ "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", +/ "ng-controller" => "GroupEnterpriseNodeCtrl"} +/ .small-12.columns +/ = render partial: 'home/skinny' +/ = render partial: 'home/fat' + +/ = render partial: 'shared/components/enterprise_no_results' + +/ %tab{heading: 'Contact us', +/ active: "active(\'contact\')", +/ select: "select(\'contact\')"} +/ .row +/ .small-6.columns +/ %h3 Contact us +/ - if @group.phone +/ .row +/ .small-2.columns +/ Call +/ .small-10.columns +/ = @group.phone +/ - if @group.email +/ .row +/ .small-2.columns +/ Email +/ .small-10.columns +/ = @group.email +/ - if @group.website +/ .row +/ .small-2.columns +/ Website +/ .small-10.columns +/ = @group.website +/ .small-6.columns +/ %h3 Follow us +/ - if @group.facebook +/ .row +/ .small-2.columns +/ Facebook +/ .small-10.columns +/ = @group.facebook +/ - if @group.instagram +/ .row +/ .small-2.columns +/ Instagram +/ .small-10.columns +/ = @group.instagram +/ - if @group.linkedin +/ .row +/ .small-2.columns +/ LinkedIn +/ .small-10.columns +/ = @group.linkedin +/ - if @group.twitter +/ .row +/ .small-2.columns +/ Twitter +/ .small-10.columns +/ = @group.twitter +/ .row +/ .small-6.columns +/ %h3 Address +/ %p +/ = @group.address.address1 +/ - if @group.address.address2 +/ %br +/ = @group.address.address2 +/ %br +/ = @group.address.city +/ , +/ = @group.address.state +/ = @group.address.zipcode +/ %br +/ = @group.address.country + +/ .text-center +/ %p +/ = @group.name +/ %p +/ -if @group.facebook +/ %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} +/ %i.ofn-i_044-facebook +/ -if @group.email +/ %a{title:'Email us', href: @group.email.reverse, mailto: true} +/ %i.ofn-i_050-mail-circle +/ -if @group.website +/ %a{title:'Visit our website', href: 'http://' + @group.website, target: '_blank'} +/ %i.ofn-i_049-web = render partial: "shared/footer" From fcd9653a795939f52ee97536dfd20616a2e46c61 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 29 Jan 2015 15:52:40 +1100 Subject: [PATCH 586/681] Add padding to groups homepage --- app/views/groups/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index d9297d7202..09268230bc 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -3,7 +3,7 @@ :javascript angular.module('Darkswarm').value('groups', #{render partial: "json/groups", object: @groups}) -#groups{"ng-controller" => "GroupsCtrl"} +#groups.pad-top{"ng-controller" => "GroupsCtrl"} #active-table-search.row.pad-top .small-12.columns %h1 Groups / regions From fd363ff6c2e8733fb90e53ed87e4de8f72104ec1 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 29 Jan 2015 17:43:10 +1100 Subject: [PATCH 587/681] WIP Groups individual page styling --- app/assets/stylesheets/darkswarm/groups.css.sass | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 4686c452a2..91fc41fc9a 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -18,3 +18,12 @@ .ofn-i_035-groups font-size: 120% vertical-align: middle + +#group-page + @media screen and (min-width: 768px) + .group-logo + max-height: 100px + .group-name + border-bottom: 1px solid #ccc + .producers + background-image: none \ No newline at end of file From d99a54accfd14147c2e9e5f45862cd8f2a9eb6b0 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 29 Jan 2015 17:43:36 +1100 Subject: [PATCH 588/681] WIP groups individual page template markup - still needs alot of love. --- app/views/groups/show.html.haml | 143 ++++++++++++++++++++++++++++---- 1 file changed, 129 insertions(+), 14 deletions(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 64960581b9..2ada84a9b4 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,25 +1,140 @@ -.row.pad-top +#group-page.row.pad-top .small-12.columns.pad-top %header .row .small-12.columns %img{"src" => @group.promo_image} - .row.pad-top - .small-3.medium-2.columns - %img{"src" => @group.logo} - .small-9.medium-10.columns - %h2= @group.name + .row + .small-12.medium-2.large-2.columns.pad-top + %img.group-logo{"src" => @group.logo} + .small-12.medium-10.large-10.columns.pad-top + %h2.group-name= @group.name %p= @group.description - -.row.pad-top .small-12.columns.pad-top - %p - 123 - %p - 456 - %p - 789 + .row.pad-top + .small-12.medium-7.columns + %div{"ng-controller" => "TabsCtrl"} + %tabset + %tab{heading: 'Map', + active: "active(\'\')", + select: "select(\'\')"} + = inject_json_ams "enterprises", @group.enterprises, Api::EnterpriseSerializer + .map-container + %map{"ng-controller" => "MapCtrl", "ng-if" => "(active(\'\') && (mapShowed = true)) || mapShowed"} + %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} + %markers{models: "OfnMap.enterprises", fit: "true", + coords: "'self'", icon: "'icon'", click: "'reveal'"} + + %tab{heading: 'About us', + active: "active(\'about\')", + select: "select(\'about\')"} + %h3.pad-top About us + %p= @group.long_description + + %tab{heading: 'Our producers', + active: "active(\'producers\')", + select: "select(\'producers\')"} + .producers.pad-top{"ng-controller" => "GroupEnterprisesCtrl"} + .row + .small-12.columns.pad-top + %h1 Our Producers + = render partial: "shared/components/enterprise_search" + -# TODO: find out why this is not working + -#= render partial: "producers/filters" + + .row{bindonce: true} + .small-12.columns + .active_table + %hub.active_table_node.row.animate-repeat{id: "{{hub.hash}}", + "ng-repeat" => "hub in filteredEnterprises = (Enterprises.hubs | visible | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])", + "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", + "ng-controller" => "GroupEnterpriseNodeCtrl"} + .small-12.columns + = render partial: 'home/skinny' + = render partial: 'home/fat' + + = render partial: 'shared/components/enterprise_no_results' + + .small-12.medium-4.medium-offset-1.columns + %h4 Contact us + - if @group.phone + .row + .small-2.columns + Call + .small-10.columns + = @group.phone + - if @group.email + .row + .small-2.columns + Email + .small-10.columns + = @group.email + - if @group.website + .row + .small-2.columns + Website + .small-10.columns + = @group.website + %p   + %h6 Address + %p + = @group.address.address1 + - if @group.address.address2 + %br + = @group.address.address2 + %br + = @group.address.city + , + = @group.address.state + = @group.address.zipcode + %br + = @group.address.country + %p + %h6 Follow us + - if @group.facebook + .row + .small-2.columns + Facebook + .small-10.columns + = @group.facebook + - if @group.instagram + .row + .small-2.columns + Instagram + .small-10.columns + = @group.instagram + - if @group.linkedin + .row + .small-2.columns + LinkedIn + .small-10.columns + = @group.linkedin + - if @group.twitter + .row + .small-2.columns + Twitter + .small-10.columns + = @group.twitter + + .small-12.columns.pad-top + .row.pad-top + .small-12.columns.text-center.small + %hr + Copyright this year + = @group.name + %p + -if @group.facebook + %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} + %i.ofn-i_044-facebook + -if @group.email + %a{title:'Email us', href: @group.email.reverse, mailto: true} + %i.ofn-i_050-mail-circle + -if @group.website + %a{title:'Visit our website', href: 'http://' + @group.website, target: '_blank'} + %i.ofn-i_049-web + %p +   / %div{style: "padding: 1.5em;"} / %img{"src" => @group.promo_image, style: "display: block"} From ed4d78bca8564b622e6cc311ab185a53bf7b7c23 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 30 Jan 2015 11:23:49 +1100 Subject: [PATCH 589/681] Decorators can't be in same file name as original file, otherwise the original file is ignored --- app/helpers/spree/{base_helper.rb => base_helper_decorator.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/helpers/spree/{base_helper.rb => base_helper_decorator.rb} (100%) diff --git a/app/helpers/spree/base_helper.rb b/app/helpers/spree/base_helper_decorator.rb similarity index 100% rename from app/helpers/spree/base_helper.rb rename to app/helpers/spree/base_helper_decorator.rb From 7ac888ae00b04ad124be1afac9cbf8cf460afc6a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 30 Jan 2015 11:56:05 +1100 Subject: [PATCH 590/681] Admin can grant create_variant_overrides permission --- .../admin/services/enterprise_relationships.js.coffee | 2 ++ app/models/enterprise.rb | 3 ++- spec/features/admin/enterprise_relationships_spec.rb | 5 +++-- .../admin/services/enterprise_relationships_spec.js.coffee | 1 + spec/models/enterprise_spec.rb | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index cb6542e3eb..e6dcbf15df 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -5,6 +5,7 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris 'add_to_order_cycle' 'manage_products' 'edit_profile' + 'create_variant_overrides' ] constructor: -> @@ -28,3 +29,4 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris when "add_to_order_cycle" then "to add to order cycle" when "manage_products" then "to manage products" when "edit_profile" then "to edit profile" + when "create_variant_overrides" then "to override variant details" diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index d126e489ae..81615afd2d 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -373,7 +373,8 @@ class Enterprise < ActiveRecord::Base child: hub, permissions_list: [:add_to_order_cycle, :manage_products, - :edit_profile]) + :edit_profile, + :create_variant_overrides]) end end diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 83576c7456..3a2d9692de 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -43,13 +43,14 @@ feature %q{ check 'to manage products' uncheck 'to manage products' check 'to edit profile' + check 'to override variant details' select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2, ['to add to order cycle', 'to edit profile'] + page.should have_relationship e1, e2, ['to add to order cycle', 'to override variant details', 'to edit profile'] er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first er.should be_present - er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'edit_profile'].sort + er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'edit_profile', 'create_variant_overrides'].sort end diff --git a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee index c7ebb1b1ba..0d8e50fc5a 100644 --- a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -15,3 +15,4 @@ describe "enterprise relationships", -> expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "to add to order cycle" expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "to manage products" expect(EnterpriseRelationships.permission_presentation("edit_profile")).toEqual "to edit profile" + expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "to override variant details" diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index f2fc256c16..15bc7c2bbd 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -613,7 +613,7 @@ describe Enterprise do [er1, er2].each do |er| er.parent.should == enterprise - er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile'].sort + er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort end end From 5ea3733c8af6a88b6a231959fdc4b176a10157f6 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 30 Jan 2015 14:07:38 +1100 Subject: [PATCH 591/681] Groups page styling WIP --- app/views/groups/show.html.haml | 128 ++++++++++++++++---------------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 2ada84a9b4..5965860242 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -57,73 +57,75 @@ = render partial: 'shared/components/enterprise_no_results' .small-12.medium-4.medium-offset-1.columns - %h4 Contact us - - if @group.phone - .row - .small-2.columns - Call - .small-10.columns - = @group.phone - - if @group.email - .row - .small-2.columns - Email - .small-10.columns - = @group.email - - if @group.website - .row - .small-2.columns - Website - .small-10.columns - = @group.website - %p   - %h6 Address - %p - = @group.address.address1 - - if @group.address.address2 - %br - = @group.address.address2 - %br - = @group.address.city - , - = @group.address.state - = @group.address.zipcode - %br - = @group.address.country - %p - %h6 Follow us - - if @group.facebook - .row - .small-2.columns - Facebook - .small-10.columns - = @group.facebook - - if @group.instagram - .row - .small-2.columns - Instagram - .small-10.columns - = @group.instagram - - if @group.linkedin - .row - .small-2.columns - LinkedIn - .small-10.columns - = @group.linkedin - - if @group.twitter - .row - .small-2.columns - Twitter - .small-10.columns - = @group.twitter + %ng-include{src: "'partials/group-contact.html'"} + / %h4 Contact us + / - if @group.phone + / .row + / .small-2.columns + / Call + / .small-10.columns + / = @group.phone + / - if @group.email + / .row + / .small-2.columns + / Email + / .small-10.columns + / = @group.email + / - if @group.website + / .row + / .small-2.columns + / Website + / .small-10.columns + / = @group.website + / %p   + / %h6 Address + / %p + / = @group.address.address1 + / - if @group.address.address2 + / %br + / = @group.address.address2 + / %br + / = @group.address.city + / , + / = @group.address.state + / = @group.address.zipcode + / %br + / = @group.address.country + / %p + / %h6 Follow us + / - if @group.facebook + / .row + / .small-2.columns + / Facebook + / .small-10.columns + / = @group.facebook + / - if @group.instagram + / .row + / .small-2.columns + / Instagram + / .small-10.columns + / = @group.instagram + / - if @group.linkedin + / .row + / .small-2.columns + / LinkedIn + / .small-10.columns + / = @group.linkedin + / - if @group.twitter + / .row + / .small-2.columns + / Twitter + / .small-10.columns + / = @group.twitter .small-12.columns.pad-top .row.pad-top .small-12.columns.text-center.small %hr - Copyright this year - = @group.name - %p + %p.text-small + Copyright this year + = @group.name + %h2 -if @group.facebook %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} %i.ofn-i_044-facebook @@ -136,6 +138,8 @@ %p   +// ORIGINAL MIKAEL STUFF + / %div{style: "padding: 1.5em;"} / %img{"src" => @group.promo_image, style: "display: block"} / %div{style: "margin-bottom: 1em"} From 6fddb491db162bfdc0acbc3f212f3098a8625fbc Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 30 Jan 2015 14:07:55 +1100 Subject: [PATCH 592/681] New partial for groups contact --- .../partials/group-contact.html.haml | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 app/assets/javascripts/templates/partials/group-contact.html.haml diff --git a/app/assets/javascripts/templates/partials/group-contact.html.haml b/app/assets/javascripts/templates/partials/group-contact.html.haml new file mode 100644 index 0000000000..64774dce2e --- /dev/null +++ b/app/assets/javascripts/templates/partials/group-contact.html.haml @@ -0,0 +1,98 @@ +%div.contact-container{bindonce: true} + %p test 123 + + / - if "@group.email || @group.website || @group.phone" + %div.modal-centered + %p.modal-header Contact + %p + / = @group.phone + + + / %p.word-wrap{"ng-if" => "enterprise.email"} + / %a{"ng-href" => "{{enterprise.email | stripUrl}}", target: "_blank", mailto: true} + / %span.email + / {{ enterprise.email | stripUrl }} + + / %p.word-wrap{"ng-if" => "enterprise.website"} + / %a{"ng-href" => "http://{{enterprise.website | stripUrl}}", target: "_blank" } + / {{ enterprise.website | stripUrl }} + + + / %h4 Contact us + / - if @group.phone + / .row + / .small-2.columns + / Call + / .small-10.columns + / = @group.phone + / - if @group.email + / .row + / .small-2.columns + / Email + / .small-10.columns + / = @group.email + / - if @group.website + / .row + / .small-2.columns + / Website + / .small-10.columns + / = @group.website + / %p   + / %h6 Address + / %p + / = @group.address.address1 + / - if @group.address.address2 + / %br + / = @group.address.address2 + / %br + / = @group.address.city + / , + / = @group.address.state + / = @group.address.zipcode + / %br + / = @group.address.country + / %p + / %h6 Follow us + / - if @group.facebook + / .row + / .small-2.columns + / Facebook + / .small-10.columns + / = @group.facebook + / - if @group.instagram + / .row + / .small-2.columns + / Instagram + / .small-10.columns + / = @group.instagram + / - if @group.linkedin + / .row + / .small-2.columns + / LinkedIn + / .small-10.columns + / = @group.linkedin + / - if @group.twitter + / .row + / .small-2.columns + / Twitter + / .small-10.columns + / = @group.twitter + + / .small-12.columns.pad-top + / .row.pad-top + / .small-12.columns.text-center.small + / %hr + / Copyright this year + / = @group.name + / %p + / -if @group.facebook + / %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} + / %i.ofn-i_044-facebook + / -if @group.email + / %a{title:'Email us', href: @group.email.reverse, mailto: true} + / %i.ofn-i_050-mail-circle + / -if @group.website + / %a{title:'Visit our website', href: 'http://' + @group.website, target: '_blank'} + / %i.ofn-i_049-web + / %p + /   \ No newline at end of file From fafbfe873544aa7eff1b4f8eb7f0453a9c22d34b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 30 Jan 2015 15:54:44 +1100 Subject: [PATCH 593/681] Simplify, simplify --- lib/open_food_network/permissions.rb | 2 +- spec/models/spree/ability_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index f6d6757d67..fffe3f55af 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -46,7 +46,7 @@ module OpenFoodNetwork # Find the exchanges of an order cycle that an admin can manage def order_cycle_exchanges(order_cycle) - enterprises = managed_enterprises + related_enterprises_with(:add_to_order_cycle) + enterprises = managed_and_related_enterprises_with :add_to_order_cycle order_cycle.exchanges.to_enterprises(enterprises).from_enterprises(enterprises) end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 0c1dd9eb32..aac9945c57 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -216,7 +216,7 @@ module Spree context "when is a distributor enterprise user" do # create distributor_enterprise1 user without full admin access - let (:user) do + let(:user) do user = create(:user) user.spree_roles = [] d1.enterprise_roles.build(user: user).save From c0a7d22a500d077a94a1a2c4f8e6abb282e1ed49 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 30 Jan 2015 16:36:45 +1100 Subject: [PATCH 594/681] Add permitted_by scope to EnterpriseRelationship --- app/models/enterprise_relationship.rb | 1 + spec/models/enterprise_relationship_spec.rb | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index ff2c1fe3fa..600ef87f11 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -15,6 +15,7 @@ class EnterpriseRelationship < ActiveRecord::Base } scope :permitting, ->(enterprises) { where('child_id IN (?)', enterprises) } + scope :permitted_by, ->(enterprises) { where('parent_id IN (?)', enterprises) } scope :with_permission, ->(permission) { joins(:permissions). diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index 3a6c48e891..e87c204037 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -44,12 +44,18 @@ describe EnterpriseRelationship do end end - it "finds relationships that grant permissions to some enterprises" do - er1 = create(:enterprise_relationship, parent: e2, child: e1) - er2 = create(:enterprise_relationship, parent: e3, child: e2) - er3 = create(:enterprise_relationship, parent: e1, child: e3) + describe "finding by permission" do + let!(:er1) { create(:enterprise_relationship, parent: e2, child: e1) } + let!(:er2) { create(:enterprise_relationship, parent: e3, child: e2) } + let!(:er3) { create(:enterprise_relationship, parent: e1, child: e3) } - EnterpriseRelationship.permitting([e1, e2]).sort.should == [er1, er2] + it "finds relationships that grant permissions to some enterprises" do + EnterpriseRelationship.permitting([e1, e2]).sort.should == [er1, er2].sort + end + + it "finds relationships that are granted by particular enterprises" do + EnterpriseRelationship.permitted_by([e1, e2]).sort.should == [er1, er3].sort + end end it "finds relationships that grant a particular permission" do From 8d9f8beff33051187ffc80068b5aa61923378939 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Mon, 2 Feb 2015 10:33:13 +1100 Subject: [PATCH 595/681] creating facebook url --- app/models/enterprise_group.rb | 16 ++++++++++++++++ spec/models/enterprise_group_spec.rb | 11 +++++++++++ 2 files changed, 27 insertions(+) diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 9a398e8c39..2b374c8687 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -41,4 +41,20 @@ class EnterpriseGroup < ActiveRecord::Base address.firstname = address.lastname = 'unused' if address.present? end + def facebook_url + if (facebook.blank?) then + return nil + end + if is_url? facebook then + facebook + else + 'https://www.facebook.com/' + facebook + end + end + + private + + def is_url?(s) + s.andand.include? '://' + end end diff --git a/spec/models/enterprise_group_spec.rb b/spec/models/enterprise_group_spec.rb index 3e38882064..4259c6009e 100644 --- a/spec/models/enterprise_group_spec.rb +++ b/spec/models/enterprise_group_spec.rb @@ -52,4 +52,15 @@ describe EnterpriseGroup do EnterpriseGroup.on_front_page.should == [eg1] end end + + describe "urls" do + it 'provides a Facebook URL' do + g = create(:enterprise_group) + g.facebook_url.should be_nil + g.facebook = 'test' + g.facebook_url.should == 'https://www.facebook.com/test' + g.facebook = 'http://fb.com/test' + g.facebook_url.should == g.facebook + end + end end From 91b35d068c19f846d11c63ca7a6db457fa7a4690 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Mon, 2 Feb 2015 15:58:34 +1100 Subject: [PATCH 596/681] Adding owner to groups Groups have owners and users own groups. The owners are displayed and changable on the group's page by admin users. --- .../enterprise_groups/enterprise_groups.js.coffee | 2 +- app/models/enterprise_group.rb | 2 ++ app/models/spree/user_decorator.rb | 1 + app/views/admin/enterprise_groups/_inputs.html.haml | 10 ++++++++++ app/views/admin/enterprise_groups/index.html.haml | 4 ++++ .../20150202000203_add_owner_to_enterprise_groups.rb | 6 ++++++ db/schema.rb | 4 +++- spec/models/spree/user_spec.rb | 12 ++++++++++++ 8 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20150202000203_add_owner_to_enterprise_groups.rb diff --git a/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee b/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee index 2d23b9c6fa..e8e462d998 100644 --- a/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee +++ b/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprise_groups", ["admin.side_menu"]) +angular.module("admin.enterprise_groups", ["admin.side_menu", "admin.users"]) diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 2b374c8687..4c9e97adb9 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -2,6 +2,7 @@ class EnterpriseGroup < ActiveRecord::Base acts_as_list has_and_belongs_to_many :enterprises + belongs_to :owner, class_name: 'Spree::User', foreign_key: :owner_id, inverse_of: :owned_groups belongs_to :address, :class_name => 'Spree::Address' accepts_nested_attributes_for :address validates :address, presence: true, associated: true @@ -11,6 +12,7 @@ class EnterpriseGroup < ActiveRecord::Base validates :description, presence: true attr_accessible :name, :description, :long_description, :on_front_page, :enterprise_ids + attr_accessible :owner_id attr_accessible :logo, :promo_image attr_accessible :address_attributes attr_accessible :email, :website, :facebook, :instagram, :linkedin, :twitter diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 4f119ab81b..58c962985b 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -2,6 +2,7 @@ Spree.user_class.class_eval do has_many :enterprise_roles, :dependent => :destroy has_many :enterprises, through: :enterprise_roles has_many :owned_enterprises, class_name: 'Enterprise', foreign_key: :owner_id, inverse_of: :owner + has_many :owned_groups, class_name: 'EnterpriseGroup', foreign_key: :owner_id, inverse_of: :owner has_one :cart accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true diff --git a/app/views/admin/enterprise_groups/_inputs.html.haml b/app/views/admin/enterprise_groups/_inputs.html.haml index 8b2db582a9..60181e09c4 100644 --- a/app/views/admin/enterprise_groups/_inputs.html.haml +++ b/app/views/admin/enterprise_groups/_inputs.html.haml @@ -10,6 +10,16 @@ %br/ = f.text_field :description + - if spree_current_user.admin? + .row + .three.columns.alpha + =f.label :owner_id, 'Owner' + .with-tip{'data-powertip' => "The primary user responsible for this group."} + %a What's this? + .eight.columns.omega + - owner_email = @enterprise_group.andand.owner.andand.email || "" + = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email + = f.field_container :on_front_page do = f.label :on_front_page, 'On front page?' %br/ diff --git a/app/views/admin/enterprise_groups/index.html.haml b/app/views/admin/enterprise_groups/index.html.haml index 035402267f..3bbf21db6e 100644 --- a/app/views/admin/enterprise_groups/index.html.haml +++ b/app/views/admin/enterprise_groups/index.html.haml @@ -9,6 +9,8 @@ %thead %tr %th Name + - if spree_current_user.admin? + %th Owner %th On front page? %th Enterprises %th.actions @@ -17,6 +19,8 @@ - @enterprise_groups.each do |enterprise_group| %tr %td.name= enterprise_group.name + - if spree_current_user.admin? + %td= enterprise_group.owner.andand.email || "" %td= enterprise_group.on_front_page ? 'Y' : 'N' %td= enterprise_group.enterprises.map(&:name).join ', ' %td.actions diff --git a/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb b/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb new file mode 100644 index 0000000000..82ffdf9f5d --- /dev/null +++ b/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb @@ -0,0 +1,6 @@ +class AddOwnerToEnterpriseGroups < ActiveRecord::Migration + def change + add_column :enterprise_groups, :owner_id, :integer + add_foreign_key :enterprise_groups, :spree_users, name: "enterprise_groups_owner_id_fk", column: "owner_id" + end +end diff --git a/db/schema.rb b/db/schema.rb index 418f5a7943..c92989be2f 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 => 20150121030627) do +ActiveRecord::Schema.define(:version => 20150202000203) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -204,6 +204,7 @@ ActiveRecord::Schema.define(:version => 20150121030627) do t.string "instagram" t.string "linkedin" t.string "twitter" + t.integer "owner_id" end create_table "enterprise_groups_enterprises", :id => false, :force => true do |t| @@ -1085,6 +1086,7 @@ ActiveRecord::Schema.define(:version => 20150121030627) do add_foreign_key "enterprise_fees", "enterprises", name: "enterprise_fees_enterprise_id_fk" add_foreign_key "enterprise_groups", "spree_addresses", name: "enterprise_groups_address_id_fk", column: "address_id" + add_foreign_key "enterprise_groups", "spree_users", name: "enterprise_groups_owner_id_fk", column: "owner_id" add_foreign_key "enterprise_groups_enterprises", "enterprise_groups", name: "enterprise_groups_enterprises_enterprise_group_id_fk" add_foreign_key "enterprise_groups_enterprises", "enterprises", name: "enterprise_groups_enterprises_enterprise_id_fk" diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index e1a9239c95..46fb0d924b 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -22,6 +22,18 @@ describe Spree.user_class do }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: #{u2.email} is not permitted to own any more enterprises (limit is 1)." end end + + describe "group ownership" do + let(:u1) { create(:user) } + let(:u2) { create(:user) } + let!(:g1) { create(:enterprise_group, owner: u1) } + let!(:g2) { create(:enterprise_group, owner: u1) } + + it "provides access to owned groups" do + expect(u1.owned_groups(:reload)).to include g1, g2 + end + + end end context "#create" do From 25189d190bba86a771a46372843b88ea88a125a6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 4 Feb 2015 09:30:01 +1100 Subject: [PATCH 597/681] Shift method --- lib/open_food_network/permissions.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index fffe3f55af..d6cc608757 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -67,6 +67,13 @@ module OpenFoodNetwork private + def managed_and_related_enterprises_with(permission) + managed_enterprise_ids = managed_enterprises.pluck :id + permitting_enterprise_ids = related_enterprises_with(permission).pluck :id + + Enterprise.where('id IN (?)', managed_enterprise_ids + permitting_enterprise_ids) + end + def managed_enterprises Enterprise.managed_by(@user) end @@ -80,14 +87,6 @@ module OpenFoodNetwork Enterprise.where('id IN (?)', parent_ids) end - def managed_and_related_enterprises_with(permission) - managed_enterprise_ids = managed_enterprises.pluck :id - permitted_enterprise_ids = related_enterprises_with(permission).pluck :id - - Enterprise.where('id IN (?)', managed_enterprise_ids + permitted_enterprise_ids) - end - - def managed_enterprise_products Spree::Product.managed_by(@user) end From 7ea143d19af67d58bdc8c65510beae7ee14296a3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 4 Feb 2015 09:42:04 +1100 Subject: [PATCH 598/681] Provide AR relation instead of array in stub --- spec/lib/open_food_network/permissions_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index 184494153b..57a314cba4 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -80,27 +80,27 @@ module OpenFoodNetwork let!(:ex) { create(:exchange, order_cycle: oc, sender: e1, receiver: e2) } before do - permissions.stub(:managed_enterprises) { [] } - permissions.stub(:related_enterprises_with) { [] } + permissions.stub(:managed_enterprises) { Enterprise.where('1=0') } + permissions.stub(:related_enterprises_with) { Enterprise.where('1=0') } end it "returns exchanges involving enterprises managed by the user" do - permissions.stub(:managed_enterprises) { [e1, e2] } + permissions.stub(:managed_enterprises) { Enterprise.where(id: [e1, e2]) } permissions.order_cycle_exchanges(oc).should == [ex] end it "returns exchanges involving enterprises with E2E permission" do - permissions.stub(:related_enterprises_with) { [e1, e2] } + permissions.stub(:related_enterprises_with) { Enterprise.where(id: [e1, e2]) } permissions.order_cycle_exchanges(oc).should == [ex] end it "does not return exchanges involving only the sender" do - permissions.stub(:managed_enterprises) { [e1] } + permissions.stub(:managed_enterprises) { Enterprise.where(id: [e1]) } permissions.order_cycle_exchanges(oc).should == [] end it "does not return exchanges involving only the receiver" do - permissions.stub(:managed_enterprises) { [e2] } + permissions.stub(:managed_enterprises) { Enterprise.where(id: [e2]) } permissions.order_cycle_exchanges(oc).should == [] end end From 4c586d1b7e0baf35812143a38a8463dc3c475390 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 4 Feb 2015 10:04:57 +1100 Subject: [PATCH 599/681] Fetching enterprises for which hubs can create variant overrides: allow all hubs that we can add to order cycle, find producers via create_variant_overrides permission --- lib/open_food_network/permissions.rb | 22 +++++++----- .../lib/open_food_network/permissions_spec.rb | 36 ++++++++++++++----- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index d6cc608757..2ea099c07c 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -20,30 +20,34 @@ module OpenFoodNetwork managed_and_related_enterprises_with :edit_profile end - # For every hub that an admin manages, show all the producers that that hub may add - # to the order cycle + # For every hub that an admin manages, show all the producers for which that hub may + # override variants # {hub1_id => [producer1_id, producer2_id, ...], ...} - def order_cycle_enterprises_per_hub + def variant_override_enterprises_per_hub + hubs = managed_and_related_enterprises_with(:add_to_order_cycle).is_distributor + + # Permissions granted by create_variant_overrides relationship from producer to hub permissions = Hash[ EnterpriseRelationship. - permitting(managed_enterprises). - with_permission(:add_to_order_cycle). + permitting(hubs). + with_permission(:create_variant_overrides). group_by { |er| er.child_id }. map { |child_id, ers| [child_id, ers.map { |er| er.parent_id }] } ] + # We have permission to create variant overrides for any producers we manage, for any + # hub we can add to an order cycle managed_producer_ids = managed_enterprises.is_primary_producer.pluck(:id) if managed_producer_ids.any? - managed_enterprises.is_distributor.each do |hub| - permissions[hub.id] ||= [] - permissions[hub.id] += managed_producer_ids - permissions[hub.id].uniq! + hubs.each do |hub| + permissions[hub.id] = ((permissions[hub.id] || []) + managed_producer_ids).uniq end end permissions end + # Find the exchanges of an order cycle that an admin can manage def order_cycle_exchanges(order_cycle) enterprises = managed_and_related_enterprises_with :add_to_order_cycle diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index 57a314cba4..edb87b8379 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -34,43 +34,63 @@ module OpenFoodNetwork end end - describe "finding enterprises that can be added to an order cycle, for each hub" do + describe "finding enterprises for which variant overrides can be created, for each hub" do let!(:hub) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } let!(:er) { create(:enterprise_relationship, parent: producer, child: hub, - permissions_list: [:add_to_order_cycle]) } + permissions_list: [:create_variant_overrides]) } before do permissions.stub(:managed_enterprises) { Enterprise.where(id: hub.id) } end it "returns enterprises as hub_id => [producer, ...]" do - permissions.order_cycle_enterprises_per_hub.should == + permissions.variant_override_enterprises_per_hub.should == {hub.id => [producer.id]} end it "returns only permissions relating to managed enterprises" do create(:enterprise_relationship, parent: e1, child: e2, - permissions_list: [:add_to_order_cycle]) + permissions_list: [:create_variant_overrides]) - permissions.order_cycle_enterprises_per_hub.should == + permissions.variant_override_enterprises_per_hub.should == {hub.id => [producer.id]} end - it "returns only add_to_order_cycle permissions" do + it "returns only create_variant_overrides permissions" do permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub, e2]) } create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:manage_products]) - permissions.order_cycle_enterprises_per_hub.should == + permissions.variant_override_enterprises_per_hub.should == {hub.id => [producer.id]} end + describe "hubs connected to the user by relationships only" do + # producer_managed can add hub to order cycle + # hub can create variant overrides for producer + # we manage producer_managed + # therefore, we should be able to create variant overrides for hub on producer's products + + let!(:producer_managed) { create(:supplier_enterprise) } + let!(:er_oc) { create(:enterprise_relationship, parent: hub, child: producer_managed, + permissions_list: [:add_to_order_cycle]) } + + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: producer_managed.id) } + end + + it "allows the hub to create variant overrides for the producer" do + permissions.variant_override_enterprises_per_hub.should == + {hub.id => [producer.id, producer_managed.id]} + end + end + it "also returns managed producers" do producer2 = create(:supplier_enterprise) permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub, producer2]) } - permissions.order_cycle_enterprises_per_hub.should == + permissions.variant_override_enterprises_per_hub.should == {hub.id => [producer.id, producer2.id]} end end From a7bb04b890a541be2eedaa291109dccbe9f96dec Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 4 Feb 2015 10:13:35 +1100 Subject: [PATCH 600/681] Find all producers for which we can create variant overrides --- lib/open_food_network/permissions.rb | 5 +++++ spec/lib/open_food_network/permissions_spec.rb | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 2ea099c07c..b917d1fc7d 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -20,6 +20,11 @@ module OpenFoodNetwork managed_and_related_enterprises_with :edit_profile end + def variant_override_producers + producer_ids = variant_override_enterprises_per_hub.values.flatten.uniq + Enterprise.where(id: producer_ids) + end + # For every hub that an admin manages, show all the producers for which that hub may # override variants # {hub1_id => [producer1_id, producer2_id, ...], ...} diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index edb87b8379..f28956d34b 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -34,6 +34,19 @@ module OpenFoodNetwork end end + describe "finding all producers for which we can create variant overrides" do + let(:e1) { create(:supplier_enterprise) } + let(:e2) { create(:supplier_enterprise) } + + it "compiles the list from variant_override_enterprises_per_hub" do + permissions.stub(:variant_override_enterprises_per_hub) do + {1 => [e1.id], 2 => [e1.id, e2.id]} + end + + permissions.variant_override_producers.sort.should == [e1, e2].sort + end + end + describe "finding enterprises for which variant overrides can be created, for each hub" do let!(:hub) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } From 22f6ece83f7445a4339b04e89f0b92becf4ed64f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 4 Feb 2015 10:16:34 +1100 Subject: [PATCH 601/681] When fetching products for variant overrides admin, fetch all overridable products --- .../variant_overrides_controller.js.coffee | 2 +- .../api/products_controller_decorator.rb | 33 ++++++++++++++----- config/routes.rb | 1 + 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee index 81cc7d3317..bcc633805f 100644 --- a/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/variant_overrides_controller.js.coffee @@ -17,7 +17,7 @@ angular.module("ofn.admin").controller "AdminVariantOverridesCtrl", ($scope, $ti $scope.fetchProducts = -> - url = "/api/products/distributable?page=::page::;per_page=100" + url = "/api/products/overridable?page=::page::;per_page=100" PagedFetcher.fetch url, (data) => $scope.addProducts data.products diff --git a/app/controllers/spree/api/products_controller_decorator.rb b/app/controllers/spree/api/products_controller_decorator.rb index 733ce365a2..af7834bb04 100644 --- a/app/controllers/spree/api/products_controller_decorator.rb +++ b/app/controllers/spree/api/products_controller_decorator.rb @@ -17,21 +17,25 @@ Spree::Api::ProductsController.class_eval do ransack(params[:q]).result. page(params[:page]).per(params[:per_page]) - render text: { products: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer), pages: @products.num_pages }.to_json + render_paged_products @products end def distributable producers = OpenFoodNetwork::Permissions.new(current_api_user). order_cycle_enterprises.is_primary_producer.by_name - @products = Spree::Product.scoped. - merge(product_scope). - where(supplier_id: producers). - by_producer.by_name. - ransack(params[:q]).result. - page(params[:page]).per(params[:per_page]) + @products = paged_products_for_producers producers - render text: { products: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer), pages: @products.num_pages }.to_json + render_paged_products @products + end + + def overridable + producers = OpenFoodNetwork::Permissions.new(current_api_user). + variant_override_producers.by_name + + @products = paged_products_for_producers producers + + render_paged_products @products end def soft_delete @@ -60,4 +64,17 @@ Spree::Api::ProductsController.class_eval do scope.includes(:master) end + def paged_products_for_producers(producers) + Spree::Product.scoped. + merge(product_scope). + where(supplier_id: producers). + by_producer.by_name. + ransack(params[:q]).result. + page(params[:page]).per(params[:per_page]) + end + + def render_paged_products(products) + render text: { products: ActiveModel::ArraySerializer.new(products, each_serializer: Spree::Api::ProductSerializer), pages: products.num_pages }.to_json + end + end diff --git a/config/routes.rb b/config/routes.rb index 0d5d034df4..7b88f6014a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -142,6 +142,7 @@ Spree::Core::Engine.routes.prepend do get :managed get :bulk_products get :distributable + get :overridable end delete :soft_delete From 7ca967007358ae36383dedf9042fb20b7df77a54 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 4 Feb 2015 11:09:48 +1100 Subject: [PATCH 602/681] Admin can access variant override only when it can add hub to order cycle and it can create variant overrides for the producer of the variant --- app/models/spree/ability_decorator.rb | 8 +++++++- spec/models/spree/ability_spec.rb | 20 ++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 6c59887730..370b219eff 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -78,9 +78,15 @@ class AbilityDecorator end can [:admin, :index, :read, :update, :bulk_update], VariantOverride do |vo| - OpenFoodNetwork::Permissions.new(user). + hub_auth = OpenFoodNetwork::Permissions.new(user). order_cycle_enterprises.is_distributor. include? vo.hub + + producer_auth = OpenFoodNetwork::Permissions.new(user). + variant_override_producers. + include? vo.variant.product.supplier + + hub_auth && producer_auth end can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index aac9945c57..4ffcc0e3fd 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -239,9 +239,6 @@ module Spree o end - let(:vo1) { create(:variant_override, hub: d1, variant: p1.master) } - let(:vo2) { create(:variant_override, hub: d2, variant: p2.master) } - describe "editing enterprises" do let!(:d_related) { create(:distributor_enterprise) } let!(:er_pd) { create(:enterprise_relationship, parent: d_related, child: d1, permissions_list: [:edit_profile]) } @@ -264,6 +261,13 @@ module Spree end describe "variant overrides" do + let(:vo1) { create(:variant_override, hub: d1, variant: p1.master) } + let(:vo2) { create(:variant_override, hub: d1, variant: p2.master) } + let(:vo3) { create(:variant_override, hub: d2, variant: p1.master) } + let(:vo4) { create(:variant_override, hub: d2, variant: p2.master) } + + let!(:er1) { create(:enterprise_relationship, parent: s1, child: d1, permissions_list: [:create_variant_overrides]) } + it "should be able to access variant overrides page" do should have_ability([:admin, :index, :bulk_update], for: VariantOverride) end @@ -272,9 +276,17 @@ module Spree should have_ability([:admin, :index, :read, :update], for: vo1) end - it "should not be able to read/write other enterprises' variant overrides" do + it "should not be able to read/write variant overrides when producer of product hasn't granted permission" do should_not have_ability([:admin, :index, :read, :update], for: vo2) end + + it "should not be able to read/write variant overrides when we can't add hub to order cycle" do + should_not have_ability([:admin, :index, :read, :update], for: vo3) + end + + it "should not be able to read/write other enterprises' variant overrides" do + should_not have_ability([:admin, :index, :read, :update], for: vo4) + end end it "should be able to read/write their enterprises' orders" do From fce7714994acf7fe0d8e00b452748ce82b7e1bbf Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 4 Feb 2015 11:20:04 +1100 Subject: [PATCH 603/681] Load correct set of variant override producers, use variant_override_enterprises_per_hub, tighten specs --- .../admin/variant_overrides_controller.rb | 11 ++++-- spec/features/admin/variant_overrides_spec.rb | 38 ++++++++++++------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/app/controllers/admin/variant_overrides_controller.rb b/app/controllers/admin/variant_overrides_controller.rb index e46d5b1a6b..234c2779c4 100644 --- a/app/controllers/admin/variant_overrides_controller.rb +++ b/app/controllers/admin/variant_overrides_controller.rb @@ -2,15 +2,20 @@ require 'open_food_network/spree_api_key_loader' module Admin class VariantOverridesController < ResourceController - include OpenFoodNetwork::SpreeApiKeyLoader include OrderCyclesHelper + include OpenFoodNetwork::SpreeApiKeyLoader + before_filter :load_spree_api_key, only: :index def index @hubs = order_cycle_hub_enterprises(without_validation: true) - @producers = order_cycle_producer_enterprises + + # Used in JS to look up the name of the producer of each product + @producers = OpenFoodNetwork::Permissions.new(spree_current_user). + variant_override_producers + @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). - order_cycle_enterprises_per_hub + variant_override_enterprises_per_hub @variant_overrides = VariantOverride.for_hubs(@hubs) end diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index aa2338a219..886f7d7e5b 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -12,16 +12,17 @@ feature %q{ let!(:hub) { create(:distributor_enterprise) } let!(:hub2) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } - let!(:er1) { create(:enterprise_relationship, parent: producer, child: hub, - permissions_list: [:add_to_order_cycle]) } + let!(:er1) { create(:enterprise_relationship, parent: hub, child: producer, + permissions_list: [:add_to_order_cycle]) } context "as an enterprise user" do - let(:user) { create_enterprise_user enterprises: [hub, hub2, producer] } + let(:user) { create_enterprise_user enterprises: [hub2, producer] } before { quick_login_as user } describe "selecting a hub" do it "displays a list of hub choices" do visit '/admin/variant_overrides' + page.should have_select2 'hub_id', options: ['', hub.name, hub2.name] end @@ -37,10 +38,16 @@ feature %q{ context "when a hub is selected" do let!(:product) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } let!(:variant) { create(:variant, product: product, unit_value: 1, price: 1.23, on_hand: 12) } - let!(:producer2) { create(:supplier_enterprise) } - let!(:product2) { create(:simple_product, supplier: producer2) } - let!(:er2) { create(:enterprise_relationship, parent: producer2, child: hub2, - permissions_list: [:add_to_order_cycle]) } + + let!(:producer_related) { create(:supplier_enterprise) } + let!(:product_related) { create(:simple_product, supplier: producer_related) } + let!(:variant_related) { create(:variant, product: product_related, unit_value: 2, price: 2.34, on_hand: 23) } + let!(:er2) { create(:enterprise_relationship, parent: producer_related, child: hub, + permissions_list: [:create_variant_overrides]) } + + let!(:producer_unrelated) { create(:supplier_enterprise) } + let!(:product_unrelated) { create(:simple_product, supplier: producer_unrelated) } + before do # Remove 'S' option value @@ -56,14 +63,19 @@ feature %q{ it "displays the list of products with variants" do page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] + page.should have_table_row [producer.name, product.name, '', ''] page.should have_input "variant-overrides-#{variant.id}-price", placeholder: '1.23' page.should have_input "variant-overrides-#{variant.id}-count-on-hand", placeholder: '12' + + page.should have_table_row [producer_related.name, product_related.name, '', ''] + page.should have_input "variant-overrides-#{variant_related.id}-price", placeholder: '2.34' + page.should have_input "variant-overrides-#{variant_related.id}-count-on-hand", placeholder: '23' end - it "filters the products to those the hub can add to an order cycle" do - page.should_not have_content producer2.name - page.should_not have_content product2.name + it "filters the products to those the hub can override" do + page.should_not have_content producer_unrelated.name + page.should_not have_content product_unrelated.name end it "creates new overrides" do @@ -129,12 +141,10 @@ feature %q{ end it "displays an error when unauthorised to update a particular override" do - fill_in "variant-overrides-#{variant.id}-price", with: '777.77' - fill_in "variant-overrides-#{variant.id}-count-on-hand", with: '123' + fill_in "variant-overrides-#{variant_related.id}-price", with: '777.77' + fill_in "variant-overrides-#{variant_related.id}-count-on-hand", with: '123' page.should have_content "Changes to one override remain unsaved." - EnterpriseRole.where(user_id: user).where('enterprise_id != ?', producer).destroy_all - er1.destroy er2.destroy expect do From a7c2a73fa802245eea7f31013a9adfad78765611 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 5 Feb 2015 12:05:39 +1100 Subject: [PATCH 604/681] make groups editable by group owners --- .../admin/enterprise_groups_controller.rb | 1 + app/models/enterprise_group.rb | 7 +++++++ app/models/spree/ability_decorator.rb | 14 ++++++++++++++ .../admin/add_groups_admin_tab.html.haml.deface | 2 +- app/views/admin/enterprise_groups/index.html.haml | 12 +++++++----- spec/models/enterprise_group_spec.rb | 9 +++++++++ 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/app/controllers/admin/enterprise_groups_controller.rb b/app/controllers/admin/enterprise_groups_controller.rb index 89d72262d5..3968cc5289 100644 --- a/app/controllers/admin/enterprise_groups_controller.rb +++ b/app/controllers/admin/enterprise_groups_controller.rb @@ -3,6 +3,7 @@ module Admin before_filter :load_countries, :except => :index def index + @enterprise_groups = @enterprise_groups.managed_by(spree_current_user) end def move_up diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 4c9e97adb9..28a92b81f5 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -38,6 +38,13 @@ class EnterpriseGroup < ActiveRecord::Base scope :by_position, order('position ASC') scope :on_front_page, where(on_front_page: true) + scope :managed_by, lambda { |user| + if user.has_spree_role?('admin') + scoped + else + where('owner_id = ?', user.id); + end + } def set_unused_address_fields address.firstname = address.lastname = 'unused' if address.present? diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 23138fcea9..336a46b9b5 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -6,6 +6,7 @@ class AbilityDecorator def initialize(user) add_base_abilities user if is_new_user? user add_enterprise_management_abilities user if can_manage_enterprises? user + add_group_management_abilities user if can_manage_groups? user add_product_management_abilities user if can_manage_products? user add_order_management_abilities user if can_manage_orders? user add_relationship_management_abilities user if can_manage_relationships? user @@ -21,6 +22,11 @@ class AbilityDecorator user.enterprises.present? end + # Users can manage a group if they have one. + def can_manage_groups?(user) + user.owned_groups.present? + end + # Users can manage products if they have an enterprise that is not a profile. def can_manage_products?(user) can_manage_enterprises?(user) && @@ -41,6 +47,14 @@ class AbilityDecorator can [:create], Enterprise end + def add_group_management_abilities(user) + can [:admin, :index], :overview + can [:admin, :index], EnterpriseGroup + can [:read, :edit, :update], EnterpriseGroup do |group| + user.owned_groups.include? group + end + end + def add_enterprise_management_abilities(user) # Spree performs authorize! on (:create, nil) when creating a new order from admin, and also (:search, nil) # when searching for variants to add to the order diff --git a/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface b/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface index 8ed9cf55fd..fe303a453c 100644 --- a/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface +++ b/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface @@ -1,2 +1,2 @@ / insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]" -= tab :groups, :url => main_app.admin_enterprise_groups_path += tab :enterprise_groups, :url => main_app.admin_enterprise_groups_path, label: 'groups' diff --git a/app/views/admin/enterprise_groups/index.html.haml b/app/views/admin/enterprise_groups/index.html.haml index 3bbf21db6e..3ccf82c10c 100644 --- a/app/views/admin/enterprise_groups/index.html.haml +++ b/app/views/admin/enterprise_groups/index.html.haml @@ -27,8 +27,10 @@ = link_to '', main_app.edit_admin_enterprise_group_path(enterprise_group), class: 'edit-enterprise-group icon-edit no-text' = link_to_delete enterprise_group, no_text: true - - if enterprise_group.last? - .blank-action - - else - = link_to_with_icon 'icon-arrow-down', '', main_app.admin_enterprise_group_move_down_path(enterprise_group), class: 'move-down no-text' - = link_to_with_icon 'icon-arrow-up', '', main_app.admin_enterprise_group_move_up_path(enterprise_group), class: 'move-up no-text' unless enterprise_group.first? + - if spree_current_user.admin? + - if enterprise_group.last? + .blank-action + - else + = link_to_with_icon 'icon-arrow-down', '', main_app.admin_enterprise_group_move_down_path(enterprise_group), class: 'move-down no-text' + - if enterprise_group.first? + = link_to_with_icon 'icon-arrow-up', '', main_app.admin_enterprise_group_move_up_path(enterprise_group), class: 'move-up no-text' diff --git a/spec/models/enterprise_group_spec.rb b/spec/models/enterprise_group_spec.rb index 4259c6009e..9642b146c7 100644 --- a/spec/models/enterprise_group_spec.rb +++ b/spec/models/enterprise_group_spec.rb @@ -51,6 +51,15 @@ describe EnterpriseGroup do EnterpriseGroup.on_front_page.should == [eg1] end + + it "finds a user's enterprise groups" do + user = create(:user) + user.spree_roles = [] + eg1 = create(:enterprise_group, owner: user) + eg2 = create(:enterprise_group) + + EnterpriseGroup.managed_by(user).should == [eg1] + end end describe "urls" do From b1b1aa5b1e9113d79cf21b9b749ebbb810333617 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 5 Feb 2015 12:07:11 +1100 Subject: [PATCH 605/681] Groups page WIP with mikael and rob --- .../partials/group-contact.html.haml | 24 +++++++++++++++---- .../stylesheets/darkswarm/branding.css.sass | 1 + .../stylesheets/darkswarm/groups.css.sass | 20 +++++++++++++++- app/views/groups/show.html.haml | 7 +++--- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/templates/partials/group-contact.html.haml b/app/assets/javascripts/templates/partials/group-contact.html.haml index 64774dce2e..ea56fe244a 100644 --- a/app/assets/javascripts/templates/partials/group-contact.html.haml +++ b/app/assets/javascripts/templates/partials/group-contact.html.haml @@ -1,12 +1,26 @@ -%div.contact-container{bindonce: true} - %p test 123 - - / - if "@group.email || @group.website || @group.phone" +%div.contact-container{bindonce: true} + / - if @group.email || @group.website || @group.phone %div.modal-centered %p.modal-header Contact - %p + %p + Container for contact info / = @group.phone +%div.contact-container{bindonce: true} + / - if @group.email || @group.website || @group.phone + %div.modal-centered + %p.modal-header Address + %p + Container for address info + / = @group.phone + +%div.contact-container{bindonce: true} + / - if @group.email || @group.website || @group.phone + %div.modal-centered + %p.modal-header Follow + %p + Container for follow us info + / = @group.phone / %p.word-wrap{"ng-if" => "enterprise.email"} / %a{"ng-href" => "{{enterprise.email | stripUrl}}", target: "_blank", mailto: true} diff --git a/app/assets/stylesheets/darkswarm/branding.css.sass b/app/assets/stylesheets/darkswarm/branding.css.sass index 0fa559240f..ceca03abbf 100644 --- a/app/assets/stylesheets/darkswarm/branding.css.sass +++ b/app/assets/stylesheets/darkswarm/branding.css.sass @@ -22,4 +22,5 @@ $disabled-dark: #999 $disabled-v-dark: #808080 $med-grey: #666 $dark-grey: #333 +$light-grey: #ddd $black: #000 diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 91fc41fc9a..d5f865c38a 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -26,4 +26,22 @@ .group-name border-bottom: 1px solid #ccc .producers - background-image: none \ No newline at end of file + background-image: none + .tabs dd a + border: none + // border-bottom: 1px solid grey + margin-bottom: -2px + margin-right: 2px + .tabs dd.active a + margin-bottom: -1px + border-top: 1px solid $light-grey + border-left: 1px solid $light-grey + border-right: 1px solid $light-grey + border-bottom: 1px solid white + + .tabs-content + border-top: 1px solid $light-grey + border-left: 1px solid $light-grey + border-right: 1px solid $light-grey + border-bottom: 1px solid $light-grey + padding: 1.5em \ No newline at end of file diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 5965860242..5f8fe03ddc 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -13,7 +13,7 @@ .small-12.columns.pad-top .row.pad-top - .small-12.medium-7.columns + .small-12.medium-8.columns %div{"ng-controller" => "TabsCtrl"} %tabset %tab{heading: 'Map', @@ -56,7 +56,7 @@ = render partial: 'shared/components/enterprise_no_results' - .small-12.medium-4.medium-offset-1.columns + .small-12.medium-4.columns %ng-include{src: "'partials/group-contact.html'"} / %h4 Contact us / - if @group.phone @@ -123,8 +123,7 @@ .small-12.columns.text-center.small %hr %p.text-small - Copyright this year - = @group.name + = "Copyright #{Date.today.year} #{@group.name}" %h2 -if @group.facebook %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} From 0f7b8804090baf31fd074cd3b1f89db6033f80aa Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 5 Feb 2015 12:23:08 +1100 Subject: [PATCH 606/681] restoring groups hubs tab --- app/views/groups/show.html.haml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 5f8fe03ddc..09e0eff0b9 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -43,6 +43,33 @@ -# TODO: find out why this is not working -#= render partial: "producers/filters" + .row{bindonce: true} + .small-12.columns + .active_table + %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", + "ng-repeat" => "producer in filteredEnterprises = (Enterprises.producers | visible | searchEnterprises:query | taxons:activeTaxons)", + "ng-controller" => "GroupEnterpriseNodeCtrl", + "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !producer.active}", + id: "{{producer.hash}}"} + + .small-12.columns + = render partial: 'producers/skinny' + = render partial: 'producers/fat' + + = render partial: 'shared/components/enterprise_no_results' + + %tab{heading: 'Our hubs', + active: "active(\'hubs\')", + select: "select(\'hubs\')"} + #hubs.hubs{"ng-controller" => "GroupEnterprisesCtrl"} + .row + .small-12.columns + %h1 Our Hubs + + = render partial: "shared/components/enterprise_search" + -# TODO: find out why this is not working + -#= render partial: "home/filters" + .row{bindonce: true} .small-12.columns .active_table From c9bbe80738b36d82c9ded91ea4d3603f3c736ddd Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 5 Feb 2015 12:28:24 +1100 Subject: [PATCH 607/681] Remove padding class to producers tab content --- app/views/groups/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 09e0eff0b9..1fd264c78a 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -35,7 +35,7 @@ %tab{heading: 'Our producers', active: "active(\'producers\')", select: "select(\'producers\')"} - .producers.pad-top{"ng-controller" => "GroupEnterprisesCtrl"} + .producers{"ng-controller" => "GroupEnterprisesCtrl"} .row .small-12.columns.pad-top %h1 Our Producers From 28b2dd40dd7ff082ca937f9333d4e5e43d582e66 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 5 Feb 2015 12:31:38 +1100 Subject: [PATCH 608/681] Kill the pad top for Producers tab content too --- app/views/groups/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 1fd264c78a..c553e063dd 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -37,7 +37,7 @@ select: "select(\'producers\')"} .producers{"ng-controller" => "GroupEnterprisesCtrl"} .row - .small-12.columns.pad-top + .small-12.columns %h1 Our Producers = render partial: "shared/components/enterprise_search" -# TODO: find out why this is not working From e19aaf6be8ebceced899a68f66eeb64edf27dda0 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 5 Feb 2015 14:53:09 +1100 Subject: [PATCH 609/681] Styling tabs for responsive design, improving groups page layout --- .../stylesheets/darkswarm/groups.css.sass | 33 ++++++++++++++++--- app/views/groups/show.html.haml | 6 ++-- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index d5f865c38a..1cdfcc8f8a 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -25,23 +25,46 @@ max-height: 100px .group-name border-bottom: 1px solid #ccc - .producers - background-image: none + + // Tabs .tabs dd a + padding: 0.35rem 0.5rem + font-size: 0.75rem border: none - // border-bottom: 1px solid grey margin-bottom: -2px margin-right: 2px + @media screen and (min-width: 400px) + .tabs dd a + padding: 0.45rem 0.75rem + font-size: 0.875rem + @media screen and (min-width: 768px) + .tabs dd a + padding: 1rem 2rem + font-size: 1rem .tabs dd.active a margin-bottom: -1px border-top: 1px solid $light-grey border-left: 1px solid $light-grey border-right: 1px solid $light-grey border-bottom: 1px solid white - .tabs-content border-top: 1px solid $light-grey border-left: 1px solid $light-grey border-right: 1px solid $light-grey border-bottom: 1px solid $light-grey - padding: 1.5em \ No newline at end of file + padding: 1.5em + + // Producers tab + .producers + background-image: none + .active_table .active_table_node a.is_distributor, .active_table .active_table_node a.is_distributor i.ofn-i_059-producer + color: $clr-turquoise + // Hubs tab + .hubs + background-image: none + padding-top: 0 + padding-bottom: 0 + +// .hubs .active_table .active_table_node.inactive.closed a, .hubs .active_table .active_table_node.inactive.closed a *, .hubs .active_table .active_table_node.inactive.open a, .hubs .active_table .active_table_node.inactive.open a * + + \ No newline at end of file diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index c553e063dd..c6f262f4c9 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -13,7 +13,7 @@ .small-12.columns.pad-top .row.pad-top - .small-12.medium-8.columns + .small-12.large-8.columns %div{"ng-controller" => "TabsCtrl"} %tabset %tab{heading: 'Map', @@ -61,7 +61,7 @@ %tab{heading: 'Our hubs', active: "active(\'hubs\')", select: "select(\'hubs\')"} - #hubs.hubs{"ng-controller" => "GroupEnterprisesCtrl"} + .hubs{"ng-controller" => "GroupEnterprisesCtrl"} .row .small-12.columns %h1 Our Hubs @@ -83,7 +83,7 @@ = render partial: 'shared/components/enterprise_no_results' - .small-12.medium-4.columns + .small-12.large-4.columns %ng-include{src: "'partials/group-contact.html'"} / %h4 Contact us / - if @group.phone From f2e3d298fab6ba7c3e366ae992225d4404a5b5b2 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 5 Feb 2015 15:17:50 +1100 Subject: [PATCH 610/681] More styling for responsive sizes groups header --- app/assets/stylesheets/darkswarm/groups.css.sass | 10 +++++++--- app/views/groups/show.html.haml | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 1cdfcc8f8a..17dcedeb34 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -20,11 +20,15 @@ vertical-align: middle #group-page - @media screen and (min-width: 768px) - .group-logo - max-height: 100px + .group-logo, .group-header + text-align: center .group-name border-bottom: 1px solid #ccc + @media screen and (min-width: 768px) + .group-logo, .group-header + text-align: left + .group-logo img + max-height: 100px // Tabs .tabs dd a diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index c6f262f4c9..151f0419f7 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -5,9 +5,9 @@ .small-12.columns %img{"src" => @group.promo_image} .row - .small-12.medium-2.large-2.columns.pad-top - %img.group-logo{"src" => @group.logo} - .small-12.medium-10.large-10.columns.pad-top + .small-12.medium-2.large-2.columns.group-logo.pad-top + %img{"src" => @group.logo} + .small-12.medium-10.large-10.columns.group-header.pad-top %h2.group-name= @group.name %p= @group.description From 1b51ea0e3a3aef266e2496320bc19ae588b00c4b Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 5 Feb 2015 15:42:24 +1100 Subject: [PATCH 611/681] Groups logo default image --- app/assets/images/noimage/group.png | Bin 0 -> 17189 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/assets/images/noimage/group.png diff --git a/app/assets/images/noimage/group.png b/app/assets/images/noimage/group.png new file mode 100644 index 0000000000000000000000000000000000000000..fe402076c004a8349900db9660ab1371e4fe8663 GIT binary patch literal 17189 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 Date: Thu, 5 Feb 2015 15:50:33 +1100 Subject: [PATCH 612/681] moving contact partial and giving example code --- .../groups/_contact.html.haml} | 26 ++++++++++++++----- app/views/groups/show.html.haml | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) rename app/{assets/javascripts/templates/partials/group-contact.html.haml => views/groups/_contact.html.haml} (83%) diff --git a/app/assets/javascripts/templates/partials/group-contact.html.haml b/app/views/groups/_contact.html.haml similarity index 83% rename from app/assets/javascripts/templates/partials/group-contact.html.haml rename to app/views/groups/_contact.html.haml index ea56fe244a..8dd8c7724e 100644 --- a/app/assets/javascripts/templates/partials/group-contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -1,10 +1,22 @@ %div.contact-container{bindonce: true} - / - if @group.email || @group.website || @group.phone - %div.modal-centered - %p.modal-header Contact - %p - Container for contact info - / = @group.phone + + - if @group.email.present? || @group.website.present? || @group.phone.present? + %div.modal-centered + %p.modal-header Contact + %p + - if @group.phone.present? && @group.phone != 'undefined' + Call + %a{tel: @group.phone} + = @group.phone + - if @group.email.present? + Email + %a{mailto: true, href: @group.email.reverse} + = @group.email.gsub('@', ' at ').gsub('.', ' dot ') + - if @group.website.present? + Website + %a{href: @group.website} + = @group.website + %div.contact-container{bindonce: true} / - if @group.email || @group.website || @group.phone @@ -109,4 +121,4 @@ / %a{title:'Visit our website', href: 'http://' + @group.website, target: '_blank'} / %i.ofn-i_049-web / %p - /   \ No newline at end of file + /   diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 09e0eff0b9..0d5b8ed0b9 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -84,7 +84,7 @@ = render partial: 'shared/components/enterprise_no_results' .small-12.medium-4.columns - %ng-include{src: "'partials/group-contact.html'"} + = render partial: 'contact' / %h4 Contact us / - if @group.phone / .row From 37dbd376e90f190401b585553cb29cd43126dec0 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 5 Feb 2015 15:54:53 +1100 Subject: [PATCH 613/681] fixing indent --- app/views/groups/_contact.html.haml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index 8dd8c7724e..b4dcda8b04 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -5,17 +5,17 @@ %p.modal-header Contact %p - if @group.phone.present? && @group.phone != 'undefined' - Call - %a{tel: @group.phone} - = @group.phone + Call + %a{tel: @group.phone} + = @group.phone - if @group.email.present? - Email - %a{mailto: true, href: @group.email.reverse} - = @group.email.gsub('@', ' at ').gsub('.', ' dot ') + Email + %a{mailto: true, href: @group.email.reverse} + = @group.email.gsub('@', ' at ').gsub('.', ' dot ') - if @group.website.present? - Website - %a{href: @group.website} - = @group.website + Website + %a{href: @group.website} + = @group.website %div.contact-container{bindonce: true} From 242fb49276924b51c22231a40ce965e86593fd71 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 5 Feb 2015 16:17:29 +1100 Subject: [PATCH 614/681] checking for undefined phone number in model --- app/models/enterprise_group.rb | 4 ++++ app/views/groups/_contact.html.haml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 28a92b81f5..ccbd79f198 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -50,6 +50,10 @@ class EnterpriseGroup < ActiveRecord::Base address.firstname = address.lastname = 'unused' if address.present? end + def phone + address.phone.andand.sub('undefined', '') + end + def facebook_url if (facebook.blank?) then return nil diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index b4dcda8b04..7f7937edd4 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -4,7 +4,7 @@ %div.modal-centered %p.modal-header Contact %p - - if @group.phone.present? && @group.phone != 'undefined' + - if @group.phone.present? Call %a{tel: @group.phone} = @group.phone From 148333467f9de3e275dc140d738a1315587da6f4 Mon Sep 17 00:00:00 2001 From: summerscope Date: Thu, 5 Feb 2015 17:06:32 +1100 Subject: [PATCH 615/681] Groups styling and markup - finishing up contact info partial etc --- .../stylesheets/darkswarm/groups.css.sass | 7 +- app/views/groups/_contact.html.haml | 163 ++++++------------ app/views/groups/index.html.haml | 6 +- 3 files changed, 58 insertions(+), 118 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 17dcedeb34..0c5aa8483a 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -1,6 +1,7 @@ @import branding @import mixins +// Search page #groups background-color: $clr-brick-light background-image: url("/assets/groups.svg") @@ -18,7 +19,8 @@ .ofn-i_035-groups font-size: 120% vertical-align: middle - + +// Individual Page #group-page .group-logo, .group-header text-align: center @@ -68,7 +70,4 @@ background-image: none padding-top: 0 padding-bottom: 0 - -// .hubs .active_table .active_table_node.inactive.closed a, .hubs .active_table .active_table_node.inactive.closed a *, .hubs .active_table .active_table_node.inactive.open a, .hubs .active_table .active_table_node.inactive.open a * - \ No newline at end of file diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index 7f7937edd4..0ed24b0d9d 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -1,124 +1,63 @@ %div.contact-container{bindonce: true} - - if @group.email.present? || @group.website.present? || @group.phone.present? %div.modal-centered %p.modal-header Contact - %p - - if @group.phone.present? + - if @group.phone.present? + %p Call %a{tel: @group.phone} = @group.phone - - if @group.email.present? - Email - %a{mailto: true, href: @group.email.reverse} - = @group.email.gsub('@', ' at ').gsub('.', ' dot ') - - if @group.website.present? - Website - %a{href: @group.website} + - if @group.email.present? + %p + %a{mailto: true, href: @group.email.reverse, target: "_blank" } + = @group.email + - if @group.website.present? + %p + %a{href: @group.website, target: "_blank" } = @group.website +%div{bindonce: true} + - if @group.facebook.present? + %div.modal-centered + %p.modal-header Follow + .follow-icons{bindonce: true} + %span + - if @group.twitter.present? + %a{href: "http://twitter.com/"+@group.twitter, target: "_blank"} + %i.ofn-i_041-twitter + %span + - if @group.facebook.present? + %a{href: @group.facebook, target: "_blank"} + %i.ofn-i_044-facebook + %span + - if @group.linkedin.present? + %a{href: @group.linkedin, target: "_blank"} + %i.ofn-i_042-linkedin + %span + - if @group.instagram.present? + %a{href: "http://instagram.com/"+@group.instagram, target: "_blank"} + %i.ofn-i_043-instagram + + %span{"ng-if" => "enterprise.instagram"} + %a{"ng-href" => "http://instagram.com/{{enterprise.instagram}}", target: "_blank"} + %i.ofn-i_043-instagram -%div.contact-container{bindonce: true} - / - if @group.email || @group.website || @group.phone - %div.modal-centered - %p.modal-header Address - %p - Container for address info - / = @group.phone - -%div.contact-container{bindonce: true} - / - if @group.email || @group.website || @group.phone - %div.modal-centered - %p.modal-header Follow - %p - Container for follow us info - / = @group.phone - - / %p.word-wrap{"ng-if" => "enterprise.email"} - / %a{"ng-href" => "{{enterprise.email | stripUrl}}", target: "_blank", mailto: true} - / %span.email - / {{ enterprise.email | stripUrl }} - - / %p.word-wrap{"ng-if" => "enterprise.website"} - / %a{"ng-href" => "http://{{enterprise.website | stripUrl}}", target: "_blank" } - / {{ enterprise.website | stripUrl }} +%div{bindonce: true} + - if @group.address.address1.present? || @group.address.address2.present? || @group.address.city.present? || @group.address.city.present? || @group.address.state.present? || @group.address.zipcode.present? + %div.modal-centered + %p.modal-header Address + %p + - if @group.address.address1.present? && @group.address.address1 != 'undefined' + = @group.address.address1 + - if @group.address.address2.present? && @group.address.address2 != 'undefined' + %br + = @group.address.address2 + %br + - if @group.address.city.present? && @group.address.city != 'undefined' + = @group.address.city + - if @group.address.state.present? && @group.address.state != 'undefined' + = @group.address.state + - if @group.address.zipcode? && @group.address.zipcode != 'undefined' + = @group.address.zipcode - / %h4 Contact us - / - if @group.phone - / .row - / .small-2.columns - / Call - / .small-10.columns - / = @group.phone - / - if @group.email - / .row - / .small-2.columns - / Email - / .small-10.columns - / = @group.email - / - if @group.website - / .row - / .small-2.columns - / Website - / .small-10.columns - / = @group.website - / %p   - / %h6 Address - / %p - / = @group.address.address1 - / - if @group.address.address2 - / %br - / = @group.address.address2 - / %br - / = @group.address.city - / , - / = @group.address.state - / = @group.address.zipcode - / %br - / = @group.address.country - / %p - / %h6 Follow us - / - if @group.facebook - / .row - / .small-2.columns - / Facebook - / .small-10.columns - / = @group.facebook - / - if @group.instagram - / .row - / .small-2.columns - / Instagram - / .small-10.columns - / = @group.instagram - / - if @group.linkedin - / .row - / .small-2.columns - / LinkedIn - / .small-10.columns - / = @group.linkedin - / - if @group.twitter - / .row - / .small-2.columns - / Twitter - / .small-10.columns - / = @group.twitter - - / .small-12.columns.pad-top - / .row.pad-top - / .small-12.columns.text-center.small - / %hr - / Copyright this year - / = @group.name - / %p - / -if @group.facebook - / %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} - / %i.ofn-i_044-facebook - / -if @group.email - / %a{title:'Email us', href: @group.email.reverse, mailto: true} - / %i.ofn-i_050-mail-circle - / -if @group.website - / %a{title:'Visit our website', href: 'http://' + @group.website, target: '_blank'} - / %i.ofn-i_049-web - / %p - /   diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 09268230bc..19cccd920b 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -19,11 +19,13 @@ name: "group{{group.id}}", id: "group{{group.id}}"} .row.pad-top{bindonce: true} - .small-6.columns + .small-12.medium-6.columns %a{"ng-href" => "groups/{{group.id}}"} %i.ofn-i_035-groups {{ group.name }} - .small-6.columns + .small-12.medium-3.columns + {{ group.address.state }} + .small-12.medium-3.columns {{ group.description }} .group{"ng-show" => "groups.length == 0"} From 504a0536932bdbb9d499c73fc9bc81fd31703d22 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 5 Feb 2015 17:21:41 +1100 Subject: [PATCH 616/681] link_to_ext helper for group pages --- app/helpers/groups_helper.rb | 17 ++++++++++++++++ app/views/groups/_contact.html.haml | 3 +-- spec/helpers/groups_helper_spec.rb | 31 +++++++++++++++++++---------- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index c091b2fc82..3b0e0022a2 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,2 +1,19 @@ module GroupsHelper + + def link_to_ext(url) + link_to strip_url(url), ext_url(url), target: '_blank' + end + + def ext_url(url) + if (url =~ /^https?:\/\//i) + return url + else + return 'http://' + url + end + end + + def strip_url(url) + url.andand.sub(/^https?:\/\//i, '') + end + end diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index 0ed24b0d9d..e1bda90404 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -13,8 +13,7 @@ = @group.email - if @group.website.present? %p - %a{href: @group.website, target: "_blank" } - = @group.website + =link_to_ext @group.website %div{bindonce: true} - if @group.facebook.present? diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 08a4335bae..c9f44dd95a 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -1,15 +1,24 @@ 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__}" + describe "ext_url" do + it "adds http:// if missing" do + expect(helper.ext_url("http://example.com/")).to eq("http://example.com/") + expect(helper.ext_url("https://example.com/")).to eq("https://example.com/") + expect(helper.ext_url("example.com")).to eq("http://example.com") + end + end + describe "strip_url" do + it "removes http(s)://" do + expect(helper.strip_url("http://example.com/")).to eq("example.com/") + expect(helper.strip_url("https://example.com/")).to eq("example.com/") + expect(helper.strip_url("example.com")).to eq("example.com") + end + end + describe "link_to_ext" do + it "gives a link to an html external url" do + expect(helper.link_to_ext("example.com")).to eq('example.com') + expect(helper.link_to_ext("https://example.com/")).to eq('example.com/') + end + end end From 30601b9203634b40ec57bd55d771a38979e00d0b Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 5 Feb 2015 20:20:28 +1100 Subject: [PATCH 617/681] tidy up group contact with helpers --- app/helpers/groups_helper.rb | 18 ++++++++--- app/models/enterprise_group.rb | 30 ++++++++++-------- app/views/groups/_contact.html.haml | 48 +++++++++++------------------ app/views/groups/show.html.haml | 15 ++++----- spec/helpers/groups_helper_spec.rb | 13 ++++---- 5 files changed, 61 insertions(+), 63 deletions(-) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 3b0e0022a2..ba884ec8db 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,14 +1,24 @@ module GroupsHelper - def link_to_ext(url) - link_to strip_url(url), ext_url(url), target: '_blank' + def link_to_url(url, html_options = {}) + link_to_service 'http://', url, html_options do + strip_url url + end end - def ext_url(url) + def link_to_service(baseurl, name, html_options = {}) + if name.empty? then return end + html_options = html_options.merge target: '_blank' + link_to ext_url(baseurl, name), html_options do + yield + end + end + + def ext_url(prefix, url) if (url =~ /^https?:\/\//i) return url else - return 'http://' + url + return prefix + url end end diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index ccbd79f198..7012e208ed 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -54,20 +54,24 @@ class EnterpriseGroup < ActiveRecord::Base address.phone.andand.sub('undefined', '') end - def facebook_url - if (facebook.blank?) then - return nil - end - if is_url? facebook then - facebook - else - 'https://www.facebook.com/' + facebook - end + def address1 + address.address1.andand.sub('undefined', '') end - private - - def is_url?(s) - s.andand.include? '://' + def address2 + address.address2.andand.sub('undefined', '') end + + def city + address.city.andand.sub('undefined', '') + end + + def state + address.state + end + + def zipcode + address.zipcode.andand.sub('undefined', '') + end + end diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index e1bda90404..9cd0babf7e 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -9,54 +9,42 @@ = @group.phone - if @group.email.present? %p - %a{mailto: true, href: @group.email.reverse, target: "_blank" } + =link_to_service "", @group.email.reverse, mailto: true do = @group.email - if @group.website.present? %p - =link_to_ext @group.website + =link_to_url @group.website %div{bindonce: true} - - if @group.facebook.present? + - if @group.facebook.present? || @group.twitter.present? || @group.linkedin.present? || @group.instagram.present? %div.modal-centered %p.modal-header Follow .follow-icons{bindonce: true} %span - - if @group.twitter.present? - %a{href: "http://twitter.com/"+@group.twitter, target: "_blank"} - %i.ofn-i_041-twitter + =link_to_service "http://twitter.com/", @group.twitter do + %i.ofn-i_041-twitter %span - - if @group.facebook.present? - %a{href: @group.facebook, target: "_blank"} - %i.ofn-i_044-facebook + =link_to_service "https://www.facebook.com/", @group.facebook do + %i.ofn-i_044-facebook %span - - if @group.linkedin.present? - %a{href: @group.linkedin, target: "_blank"} - %i.ofn-i_042-linkedin + =link_to_service "https://www.linkedin.com/in/", @group.linkedin do + %i.ofn-i_042-linkedin %span - - if @group.instagram.present? - %a{href: "http://instagram.com/"+@group.instagram, target: "_blank"} - %i.ofn-i_043-instagram + =link_to_service "http://instagram.com/", @group.instagram do + %i.ofn-i_043-instagram - %span{"ng-if" => "enterprise.instagram"} - %a{"ng-href" => "http://instagram.com/{{enterprise.instagram}}", target: "_blank"} - %i.ofn-i_043-instagram - %div{bindonce: true} - - if @group.address.address1.present? || @group.address.address2.present? || @group.address.city.present? || @group.address.city.present? || @group.address.state.present? || @group.address.zipcode.present? + - if @group.address1.present? || @group.city.present? %div.modal-centered %p.modal-header Address %p - - if @group.address.address1.present? && @group.address.address1 != 'undefined' - = @group.address.address1 - - if @group.address.address2.present? && @group.address.address2 != 'undefined' + = @group.address1 + - if @group.address2.present? %br - = @group.address.address2 + = @group.address2 %br - - if @group.address.city.present? && @group.address.city != 'undefined' - = @group.address.city - - if @group.address.state.present? && @group.address.state != 'undefined' - = @group.address.state - - if @group.address.zipcode? && @group.address.zipcode != 'undefined' - = @group.address.zipcode + = @group.city + = @group.state + = @group.zipcode diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 9dc60a7384..8e66f18a12 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -152,15 +152,12 @@ %p.text-small = "Copyright #{Date.today.year} #{@group.name}" %h2 - -if @group.facebook - %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} - %i.ofn-i_044-facebook - -if @group.email - %a{title:'Email us', href: @group.email.reverse, mailto: true} - %i.ofn-i_050-mail-circle - -if @group.website - %a{title:'Visit our website', href: 'http://' + @group.website, target: '_blank'} - %i.ofn-i_049-web + =link_to_service "https://www.facebook.com/", @group.facebook, title: 'Follow us on Facebook' do + %i.ofn-i_044-facebook + =link_to_service "", @group.email.reverse, title:'Email us', mailto: true do + %i.ofn-i_050-mail-circle + =link_to_service "http://", @group.website, title: 'Visit our website' do + %i.ofn-i_049-web %p   diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index c9f44dd95a..89b28ae3fa 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -2,10 +2,9 @@ require 'spec_helper' describe GroupsHelper do describe "ext_url" do - it "adds http:// if missing" do - expect(helper.ext_url("http://example.com/")).to eq("http://example.com/") - expect(helper.ext_url("https://example.com/")).to eq("https://example.com/") - expect(helper.ext_url("example.com")).to eq("http://example.com") + it "adds prefix if missing" do + expect(helper.ext_url("http://example.com/", "http://example.com/bla")).to eq("http://example.com/bla") + expect(helper.ext_url("http://example.com/", "bla")).to eq("http://example.com/bla") end end describe "strip_url" do @@ -15,10 +14,10 @@ describe GroupsHelper do expect(helper.strip_url("example.com")).to eq("example.com") end end - describe "link_to_ext" do + describe "link_to_url" do it "gives a link to an html external url" do - expect(helper.link_to_ext("example.com")).to eq('example.com') - expect(helper.link_to_ext("https://example.com/")).to eq('example.com/') + expect(helper.link_to_url("example.com")).to eq('example.com') + expect(helper.link_to_url("https://example.com/")).to eq('example.com/') end end end From 9547f91f46c63861614c2ab52bef1573a39939e8 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 6 Feb 2015 13:31:33 +1100 Subject: [PATCH 618/681] Groups page rejig the layout to make contact column skinnier in most use cases. Obscure email and website with CTA words for fixed width on content. Styling for anchor fix global issue --- app/assets/stylesheets/darkswarm/groups.css.sass | 6 +++--- app/assets/stylesheets/darkswarm/typography.css.sass | 2 +- app/views/groups/_contact.html.haml | 7 ++++--- app/views/groups/show.html.haml | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 0c5aa8483a..7cfcb3a443 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -33,17 +33,17 @@ max-height: 100px // Tabs - .tabs dd a + .tabs dd a // Mobile first padding: 0.35rem 0.5rem font-size: 0.75rem border: none margin-bottom: -2px margin-right: 2px - @media screen and (min-width: 400px) + @media screen and (min-width: 768px) .tabs dd a padding: 0.45rem 0.75rem font-size: 0.875rem - @media screen and (min-width: 768px) + @media screen and (min-width: 1024px) .tabs dd a padding: 1rem 2rem font-size: 1rem diff --git a/app/assets/stylesheets/darkswarm/typography.css.sass b/app/assets/stylesheets/darkswarm/typography.css.sass index eb1885d361..a3f7085675 100644 --- a/app/assets/stylesheets/darkswarm/typography.css.sass +++ b/app/assets/stylesheets/darkswarm/typography.css.sass @@ -22,7 +22,7 @@ body a color: $clr-brick - &:hover + &:hover, &:focus, &:active text-decoration: none color: $clr-brick-bright diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index 9cd0babf7e..cb6c25a6b5 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -4,16 +4,17 @@ %p.modal-header Contact - if @group.phone.present? %p - Call %a{tel: @group.phone} = @group.phone - if @group.email.present? %p =link_to_service "", @group.email.reverse, mailto: true do - = @group.email + Email us - if @group.website.present? %p - =link_to_url @group.website + %a{href:@group.website} + Visit our website + / =link_to_url @group.website %div{bindonce: true} - if @group.facebook.present? || @group.twitter.present? || @group.linkedin.present? || @group.instagram.present? diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 8e66f18a12..5c6a6eddd9 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -13,7 +13,7 @@ .small-12.columns.pad-top .row.pad-top - .small-12.large-8.columns + .small-12.medium-8.large-9.columns %div{"ng-controller" => "TabsCtrl"} %tabset %tab{heading: 'Map', @@ -83,7 +83,7 @@ = render partial: 'shared/components/enterprise_no_results' - .small-12.large-4.columns + .small-12.medium-4.large-3.columns = render partial: 'contact' / %h4 Contact us / - if @group.phone From 755adf4287f30ccd2c64069df319010434b2fa30 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 6 Feb 2015 13:41:58 +1100 Subject: [PATCH 619/681] Index page stop-gap improvements until we get more variables to display in this list. Commented out search field not working --- app/views/groups/index.html.haml | 33 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 19cccd920b..49bd277532 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -8,25 +8,32 @@ .small-12.columns %h1 Groups / regions %p - - %input.animate-show{type: :text, - "ng-model" => "query", - placeholder: "Search name or keyword", - "ng-debounce" => "150", - "ofn-disable-enter" => true} + / %input.animate-show{type: :text, + / "ng-model" => "query", + / placeholder: "Search name or keyword", + / "ng-debounce" => "150", + / "ofn-disable-enter" => true} .group{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", name: "group{{group.id}}", id: "group{{group.id}}"} .row.pad-top{bindonce: true} - .small-12.medium-6.columns - %a{"ng-href" => "groups/{{group.id}}"} + .small-2.medium-1.columns + %h1 %i.ofn-i_035-groups - {{ group.name }} - .small-12.medium-3.columns - {{ group.address.state }} - .small-12.medium-3.columns - {{ group.description }} + .small-10.medium-11.columns + %h4 + %a{"ng-href" => "groups/{{group.id}}"} + {{ group.name }} + %p + %em + {{ group.description }} + / .small-12.medium-3.columns + / {{ group.address.state }} + / .small-6.columns.text-right + / %p + / %em + / {{ group.description }} .group{"ng-show" => "groups.length == 0"} .row.pad-top From f292be8c92c00ee0f5f4721a736a4567ec5f1439 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 6 Feb 2015 13:50:34 +1100 Subject: [PATCH 620/681] Making styling of underlines for contact same color as HRs, bit lighter and brigher --- app/assets/stylesheets/darkswarm/modal-enterprises.css.sass | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass index 305566ad58..e0090ca163 100644 --- a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass +++ b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass @@ -15,7 +15,7 @@ font-size: 1rem font-weight: 400 color: $disabled-dark - border-bottom: 1px solid $disabled-dark + border-bottom: 1px solid $light-grey margin-top: 0.75rem margin-bottom: 0.5rem @@ -67,8 +67,8 @@ margin-bottom: 0.5rem overflow-y: scroll overflow-x: hidden - border-bottom: 1px solid #999 - @include box-shadow(0 2px 2px -2px #999) + border-bottom: 1px solid $light-grey + @include box-shadow(0 2px 2px -2px $light-grey) .enterprise-logo, img float: left From 78877f591bab57ebec5bdab79b26be0fa8155bcd Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 6 Feb 2015 13:58:04 +1100 Subject: [PATCH 621/681] Fix template so website links work to push to external pages --- app/views/groups/_contact.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index cb6c25a6b5..8a612ae194 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -12,7 +12,7 @@ Email us - if @group.website.present? %p - %a{href:@group.website} + %a{href:"http://"+@group.website, target: "_blank"} Visit our website / =link_to_url @group.website From 90ba1d219820e1b955fc69021b1870a1ff2593dc Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 6 Feb 2015 17:15:40 +1100 Subject: [PATCH 622/681] Updating the groups header to behave better with a square thumbnail logo image. Intention is for a square image constrained by PaperClip as per enterprise logos (later down the track) --- app/assets/images/noimage/group.png | Bin 17189 -> 25072 bytes .../stylesheets/darkswarm/groups.css.sass | 10 ++++++++-- app/views/groups/show.html.haml | 13 ++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/assets/images/noimage/group.png b/app/assets/images/noimage/group.png index fe402076c004a8349900db9660ab1371e4fe8663..db71856deb9743be94549e4635c4c4fa27640a9a 100644 GIT binary patch delta 24441 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�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 RN;9gb!x@?buk@Yb{s$e$l^Os5 delta 16496 zcmZ{LcQjmI^e;v)(HRn*5rmmRBx=;rMvvacs6mkEb+o7x(T(2Q7g3_u(ME|bh>++l z(L0gk`MtN^`|q8#?p^opwa-5Lp7S~9?DaYO9{1yyM&WbC>T0V}k+YKH;o(uKt10W_ z;SoUa@bKkH0r!%$t!DgsE?ELjm?TUTrT|e?cnTGVLRG{S#U#X4RA3MV*i-0J@u!L@ zAq0>9uad(5SE-%Af!9G23bB1|XD=*fBaRdnx3iHDwt0qpDr{$i6qT@rib3t5HYv)0 zM3mrfRet{bd2n#B_rJ^j{{F9Dzjk(Z?(XjHUEJQ@-re2({rmUU*4Dzp z!q=}~=jZ1a7Z>N|=I*Pkudjdk@?~{(b!~0!UcRxhF*`fEva<61`}fVw%^yF0EG;d4 z`}S>ldHG&=FS&o6nVFfMo}NNY{ZE;koSc}L7#|-W8yoxczl*Q0@8{2-{rvpy@5jf- zmzS4+|Niy&_m`5Ay1u?XKR>^?xHvmI^YQVyy}cbB9p&QU3J3@&EiJ`hFenu2@bK`% zhY$b#_g`IIT}DR6y>eGq7XpEZiHW(nxv8(O4-E}HJv~iNN2MnwCTeSIOG-*+Wn~o? z7w6{YW@culrKJrH4mLD2bar-jcXyYSl~q<&1_uWZ4-ZF0MYXoJqS5I1`1p{JkY~@H zxx2gP=jWG{lq4l3RaI4WbadcwxO=l!R#tg=c|$`(oSd8|CnvSFwb9YhZEbCqmX>aA zZhd`y_d?XYgCip&_fEyd#f621_4fAO*U{3_YH4XnNJ!}E>A7#Lrltmq#SRP%G&VMV z`t&I_HnzRJ-NVBpIXU^>%fiA!9UYx}O*kB0US3{NQDJUwo|>9^|8RA6m6DPY9v&VT z7}(t0TwPslV`KC2e!sQ2#+3JQvfip+?Ck97>dMW{{qNsDZ*T9&$jEo^-o1MDik+Q3J3Cu+jPy4ip3H^1 zvcgNhZ~Nb4bsjq2KKIa+_n>oKL_pHtQ35|dY9JD*(|e^>5c@bb8P+x@k@0QYSt%7& z@hV04TUpIahkdq@Y>TgN$C7M!V91A+7gvF`nf{L3vU$D^@Z%Hb7JSY8gt*czu6g_+Y5szix_7OScY)0&}Ie?CgMerad@FO#jK zW2N=)etrAmq0a(h>ZH(CWoVp5RkYd;6}o4#q1Qr2Lla4?AR2ClK1)9FR|opB(D<*l znM4ECG>s!#04P~L2CYd+0y-dv;DP-!nZeBbb**iU6@)e$T(%v0 ziEm;@9>%CIL2<+>2BGST*x`Q7TJXaQ>TD;nxhr!Hrz+?}8|R+}rF_=tFg_F?H717$ z*BHJ=mn_pw+DQZAgj{U#1Smit*P7+2da-M^XnuiAL#QZZ3S9!WmT3}^{Z?nvJReLE zA0>UWX~zr2iSyrJD+nlgxnj3fi;f9mgsG?pX%gbl^snrYcb*DhZfEL+_c^nJn2<78(oc6bCcPJ`81%PsC4!CTBi1zQldO z2QkJn3uo+BxGvfWOGzL^3kOQPxIiFs2sX0hWQM*^Gx%rBj4mlu76EloC&R-%!8&!y zH;IE%IKANGPJ@vYP$uAtVOS_WY+r@VlCsYA)v7}%jLjZC7>ZIt=cXX*(7dFOt>3h225F`1 zlCrR(#Oh;u7+k|@@GBz7^JnA>Z0myGpk^llIPSQJO5|dgM?>)&q?lS%5IS*csh5*V z*9i=95XLV?{fAJt3ShKCM`-p7kU0%KAA3A-9ycIg1>^vp1XF?VqeyHw2h~0|N4dfr z2x5{V^xk1gO7`EAX<7}c;46V0EF_FH%(2b&ZUjkWyf{>tyzV7VnXlH43gQi5nz-;UE`cRMK~x2xaN#@cCNusi3-Cv z?9>HI2~!OXBqoHhlmd&}jj5vIVg;COf)HMel+U{fFJ|Tu!W9@19w=52=Bc1g4)P1t zW9p<6(S7ooYPrA(jf7LHp%r54u^0#5NmlpTCs?&Epyw->IHqv42KrdAWG6XUK|8Jz z^-;PH0Jm1Q%1Vig(^5v>u5!VGiA0&SLic`6hvIuI&d+d@J;f7cg~$f$y#qeRB;v!Y ze>Jy;j*wQZI4M#xpo0=hKC97z%hpQmRmwpoiAnL(o4k86>iDouCw{ov1E5|Itv}fa zpV1L3ayp4N{vOaCP*_fig&~>-eTAUs^fbo7r0S_k|-@(-rs zm*J5?g0Xl!moqa+Bup4akG$pNDk~s^LhVO{+ESkBhQ9Lm=S1RhIq1rMdO(TGPvX_| z@c=*Fqzf!~Z99yKdt(fpDh1*JZyu>LAxgxY^D|iA#J)Z?auMZ*CKFl9qqH?*P2OMO6V}}MTeb6fs1BncC^sIFqR<{!AYi`_<_JL_NfO2 zpF@=*CFj8KQCwIkbXW~_i^uaGjI%&$O>uJ;GZp7P-g}!&gsYAWBgBY!*G4_Q!ecQ z^ua2C4hL*-s(!R#Z3=>j9dwbQ4ZRAM9yt7&%o+N+FEbf5aJ{Ww#|?7O@5AdQ6hMT(edRcgKihWsRVllu#``Uz znkf~YD2k(j%P{~q(BhN?^!9$QcCrCwr^o49Z>sv5a~bi01z8v_7Anlzlq4BBOs+D$ z_8UrNb#OS2d2FKja(wC=drCNdW$a6|^MSThy;0cO3kXd`A%`(a)?px7AD z+0FZ3AQu<$%Ykb-DH?M0al38CBe);9?k!_HQ3$mrFH09Qqi z@&bs8#cdxD1Cfm?=*FK^cx4NZ(A+#>T_~rh@`T>U!}0@!BkCW29~R*mduK_~{k2PsdZxW$Mst-dDqT@DeNe9Wm;%K(R&)El@H`7r9FBvMWA< zpF&9tV?wAcZKt2p!dxrFzdq6lvr%HIKq1%XLIS3gMhLc4=H%3$v7(D4hz6R{^4zM0z@Jx%z{p_ih;yseolm4=fY$%SySh3oiHRdY zi9q9+SYo)IXl=rXCPsk@!_Ee@44n?Z*V7C2FY)ZJh5LT2?8W;^$jmdWKAHw3!zdWi z(gSpoqc#z#R^bu&E|t>vSVYmjw5|d+f{TcuoKSPSJnA_kN4ZTBFG%v$N6k zgCa7>wi&#)AX+G`Df)1~)zfh(VQ9XHm!1aw6+XZPdqdCI`7>A^QFMs}+DMfz9C|Ie z`|T9&4il2l8^QmCPG>+y@ruES_F|&hwyuU?$3S!G;0MlZY|`_BxgTVlAm3s3Xx;(r ze?O=QXRC;8fha`L36jA^(B*r*k0wR1_iv>%5_X%uxG+ZOX_fkPD74bQm=tn=?;a8! z)~j-Oxu@DL9i%jOGUer1$HWE1&s0-Fo6ec%BiecJHrQ83FIA-pAMRdj=|V-MxO%;w zabRljAk%@gVS6E7frBspOB+piu%_nyLVu5PMexJ$5h}W}3Am^QeM$nbirUKvU!^&Dnn%m>4=bf={!o>`yU!U}?MT-LI0C z4yE1`GFN?I{5glKH=F<5;BKuV)zgUKtDJbMuj(9i2tdErPphL z6z;fXbF(V@ZL#eY*6W!ZMg)qH=Y#u>V;>s@EcproC|wP~qLA-4@xY7Db|PY8hd`9$ z4)sCU{DGiLfqj<)K}sARd=<@b`3$DOLwRL;G*=<5K{G4u>-q#EkMxohpuxN}h^Tz& z6Z4Vn#kU__mMjQCUt}W9EE*Aq2a)xrAl6E4Um8DZRiZ&AK6Lm=1gFM>*Aqp_DR}6B zyI*LNg}E})Ak7f0)Re3*k1V^`zN4(2K?-ToY~R^@%Y=j>8W0?F)%^hcG1u3^dTIO0 zEhpriSNur|WBcIr7((LmkPIdQ(tvdRe0XnVw@$lO*6oApRRUP3{ioe+z0cRzJ>vwR zMGFLJCip0^*|Vm|I;Cp4kR$a=6b43=;_3s*y>tKiC8NP*o}a=Df$lv=sSv`<&V1Y{ zfyF|~)J=e+bu=BkrTZrpNf}rMqxc2!jTJD));xYW^z0d?xuTbWT^wT~8@}e=VOHe| zg48o%w8(I6f4{sbCn_U?Q8+|@GB|LV6@~~yeyU)e{(b+2tBk13@I2r!MmHMcAQWIe z>#POi<|>=XGJ3Yw$F_|6oaG2y5UhFE!34C2y$${%Z#pPWfl;PFuUmEa9FO?2 z-^c!u#HXM4m@p3QVWZ+a{zv$Sett^W#>3VsO~_w#$(Xt@M2p%%3zB-8^97Ay{cQ_c zGdieVLjGij|MFCSO29L!X7@Rb2~&3M>Wz_p*jj(5ReGVf$b>b?OIr$|APoDPL`DQDfqf>kwanpZkR$elH6urpMqf0vR&N=BYWtDEZ+t0k_JyPWJt6#8i_<(Akk!0h+ROc)(^ZBrWc)Zj zch1F&k-W?LadiG zhZrpWxZPlQ|8K|L&igZUnc~vmLQ2-n4uiE2XXK^RQyR(wAJDx(SKaMfMC@X>x2huHGZ>m{kR8(vI zzbSuZE5fVqO@F6uKzOt4_`q_1ckA6{+GUEr-Y=I}=nvWY!d}$|LLwU80aeF5ybNpA z;DM2d<(h_I)oN)%%%SSt5+zZY6!%E?9P|JaVkpD=HrReRI-OnXIhqJ118!fbv{pW}BG4 z4QLCg2qD`kW!oy5(C|Vi2KaL#2sOK4hP90`deOj8U=51_q_8V7=lrXeqc|cmy84)# z0#fH~bu5ZT6}B)T6r(Ym$T6%8_YKeUG6kFhE$RW~faWe9Js0K)3C`8(J3SEH1}tR} zgXHQBFbg_Ji33M^v;4!t_sGPnno)~G4)_4ZS#~On(>jI!hX)_-9Xi18bR~zt4BZ(d z)rE1XXfn*igfy-Hi6fs9L{~)5AK#AGd%VNHcX)(!8W>vXbr>^7!8pr8ioUxh%+*72 zAh<`=n`ma)3E++;R-I5{%CasWEAA0Bd@N;qFXeFWm&XGY1AgP7y)U^h*J%NOVwt)G zGZNt1pUg)Xh3CMzPO~>p)d?J`0tARTVOmj+JcRxKVjuq2`F=-J&wLJh{b4aX|Hnt- zV%dU(RjcNkBiBR+UPj+l31dg%A!7E0u7W?_hEXE1`d*FfqP-mr*z$sVd2wE(3v!#N=#S`nFfq(J4~GZzE@WxHz^=R zhCoX)dPWSFCnSyrz&tx$fsj)4r~TL)iltP+0}0@52qB~JgrKWAn}A}PVu(Qt;-%_C zF_Pj*wQg!z4Nc&5wGO*dA>0QBS=7SPe z8TI6Fy+0GwHrnCufB;dpf^Cxj7cP@yu?eGz@d}}OC_7%=aS8%4CsN!+rl~nRn+{&b zxgkz6<}=M$LNdKCln+FY)MNYO=gQf^=I`)9YGlR4^dkHtiepA5F|X)x-p?^ZLQBtF zs+M0-l&b6unxBLdpiN;7E$DCBedao?jn=a)ZVv-oOTxqv>EEU%~ z@xV;?U_ zTY&(vFcE%NWw96G%@UqqV*E1W7pnSmoup_8eyk~k?V${C zGd!GE-u-T61+^I#mtghaM^&^sbpkUw@4+kybkf$30{WxLxXYk(K(_ATc)@Nb0!j$a z2o|%=s~&?tUK5e>>X8I4T6nW4SI>n2Z9|sQi|ABA2O)(&tGAc%j%ws!UiTV3%ZiT6 zj(^-4(|Ju#$IGMsA}aqfOfMSK%gIG7=wQCvR1Xv9-bU^c(I8O7z4$N!kN^?Dw39v^ znt(A&T6`JKEeB*{E@W086w2hL`bj`Jt_DJ+a)AmkX34KGpf%*a3JZcDB1xxu4smOz zUdgwu5U}Q;c8uoj5?LimnTsH!Me-so?93b(%q%Pjtq6#LRG4ZKIE)1ucONWKP(pKn zL@Zw~!ULjFG-C7I8%5_cM;ApD5EZRZy)bnZMr0(iFZor&D)$!m2^|Pqa*o)vcUJ(w zKrlrIp+tB})rogh>SrTH=6B3wnDBfI$@01Q0TqQasOk>le&|2RbfC`#$-{Om`bS8IdvNpd?!UvGQl1+GUaTHhCGj8bL=d7^}EK;o^jVl#AoqPYwB}512ZT2|SFwWL87FUEAIuyNu`a`pnZ@gn4$@%9#^JXTjLFdQ@9`jzsIqRaCx z_O;D7gOlr?1Yz92v^CWWxo`W~sDvICK2)f!Q}&Qt0_sstoOOIbfIoZJxY(zY#qieE zcSevnJRX8#QfTSJH&x$(oh~T09tK{V)3Dk&**%^ zs^Yg{B{7Yp@!xs&U(1CKF(+jdNnvRqlH@o(&>H<&q%E)9^SvNh9%5S%P6zgC8)ZV+ zf9pt#d7(;H9jIZMccZW&G{H8XU z-gx+8gGj;XLfRo<{zw*-*negecBmo=A+g~g@L$5*0utOz=NNE;m1#t<>^L>eZy zVK6g}{~qYiRZJ}=lXX`;&imp6sz<6~!u;FJy6p>~AoQ&D#4plU?YV@P-X_xN&{AC(LD@ z=Q{BzGoK72oYZV92pI~>vkRV@E6rk|qGo;svFTJ}I*>uU?Q2*txUwooc^^5Fj1&VSxJ(Q)I9g5neEcSqhb$A=f618k3ZAIvgSOMSoCoZbAh4KDh(qsKAHA$@5R zG#$qqi%mZ^o{_Z*R1^T9{xH2rAPHr*nYBQ}%s&y+ zt|iCP^5R+>Xnx%hPNyFH;HtAfq+9uK+aTMf)Sr=kf}0)g16r5B-z28xvae;|o~i{+ z&i~raHvRUCq=sW6uOGFNXS-rTZO%C{{%!K}9yaKXO(^7SnYc}3sY7@qaJz=pj9}Yw z!KQHg(Wv+5_&-yBxi1>9-9xjxJKw9kH&6KH7i`lrTB?ORQ&sxUSpVGi8E@uukYT{p zj(7S-R3*S0NWPoKMyUKTs2SgR@&G<`@5J;+O$#I zM}oM!#g-inmWiTxgBF(ljQrwb|3~*wRDcT93EJm|Rz>Id6y9xJuG1ZV(` z14-!n!;~xPZ-#|I)|&(DQFPSbk?tJ7&&0E5?ZMlLIRlKS9#xAMZM%^cx10(6=WrSI z_#fqZlkE2))vtixvJ*PFuM^kW)VjOn)THiArR**>TYYB!tpB+q-V=#y<_ntq;|%HU zKYc`=k1}yI;s1Kp|4a2bWIna;Pe{31S701!7Z%{OOaJ>}ddkPvJUu2Hvrgz6tW%19 z=F+t#Wzd-;eFQ9EU8RfM>{nGrkZFL4)nKt1^>QtLPQ zLV&8t{^F5cWw|gU!f3P{5S;Wd?vJ_Z?%wlV840y~Uu6>956*wx-#+vte#at0Cb8$2?Dk&* z1OCcKP~o4&xkLr$;FgH8s+|%<-e|WwERw0_K{Y`?0-r!g4ir|7lYmCah*EKsk8aE)E2n1zDTSq8a3dy|UlDn-X;uD9=(%>^vsdK+ zn#8n?k^sL<(1QS%A)O$?*&%GF(&&ob{-6X(skb`iv;`hWn6GHbt9Z17-LJ=*Sex-* zcDf9F%{cSPlvn*ZV*2PA@rV1c{+o5~X7ittrl-BlcSkpkU8V8lkO>>v*U-Wb1GQ%5 zq?W5l_*9(n0D2>%PTYD77cuq}Mc zzN+htZWv`>mx{>ewU9onj%p~CI=x1j2g_hyw6z?^0zl7V6vr}=Cme^4cew$2BtT-E zK;SGn#3-gs^Qe?t$WP4gkBIHc0Vk1|yYStEt3d3SIoYUzz{G=3?76~Lxh-!QNTOtN zpIkHM>QDczq!?2#7o7`RJi5C!HNLLP-&#uN7z=rbWtSd59S=15q15(d7j1(INk6}> z;KAX0P7-bNT``FuAijITg08VwVpn1zbfM~j8dii3%J?|*!Mi?vQ6kT<@LptfX7-~o zJ)hjr4wI=B)N(>c|KS50JwFV%(X?fW*WtkGu%SH93bM&(d_y(cIlo2Y`7K;_%u@Sc zgimSle&i|v2B(kGgCD!TN1=KbSQd00JF&s2FWNBi0aENEZ+HCik~_-|f)cx%8U!JKqR!g{qnGMYajD{5(rOB{(|@#j{8JG zfq)fQbqfS0i*#bfTHmu{j@uSxeJJNq~Jyq>s z?pK090tZ4Sq&^b{mH$a*T+20_n2Q_<52j>84^l!?utk*RIxsEnk$` z8+V+yjB%*h{j?H$tfPUQB5xcRA2&p7=gxTcQo~yR57OFKiNu~rRmU5VJ4jypXB3*B zQ;2_-iPKP>5L0-ab`dPsv@82KR74@qbvzL$?RLd4Nc$lI<_Bq@ z+0f8%gLs*}@10`Iti)#@qoRO=;~{ri)MI0BMA)uo?K@pA4I`L$Uu4KSW>RglbRf%I znCusH@>^7$=LIneh~w0BbIJ2iTDQ;CQQB4Umos*Y-h_Yq=^n+kh~56KHT`VTvdqu9 zXF`rc33`*I07~BWS`G91SpOi|MyYLu_a@4|5Vqd*CV-PgB)tQ+VA8Ic8>8CvZue>{ z4u$jTYX$!})eP5BSge>ai>O_wT*~Af$L{TwyI=)C4zXGdC&G4qyX{ST`TJtzB<3MSEVkOx3Z{kK{s>&8>Rr-dAt4|iT5eXomPy+O(2W-0ADa&N5HR>!^?XL|S+ zpwf%Z!U%*8)1xRp{&M%bYc2B3Wn%sG%e7$g?F&nw2zz}GFZD!&w}L28`<2%;H8It7 zc+>fs#UZlwZi%j&|Dfu|`{$_3r`;D%e(9!HE7@F4S!!PN(ClCfZoDR?&s+@`x(~Km z=D+M&)q2w&Ir%)+;#Xzyp(ZCdGhR4mj7RbL*ed;;ZKXl?tg{K|LGoyL7(Azl%Hmc$ zW-g{UNcI_7-eeznet$po{Tia9G$wpBfEc;J)~u~jU;p~yxRLkk&jc0ypkodrcrp$a z)I2P!e;%=h!*l>_tr}lOHuHt(95JdF>72q^szbNB=??ZNiE(bxxg909`-~Yb!Kmgz z_gu|h*YwnXJidiVQJ8<;vKx)!TyOBp7=+%3{)gF234wg9`uAc+$HH1SX7AU=$g%IJ zE~NO)l}1DhPySkOjyHJ5!t1-u*`29l2~%gsi3};_M8KV}G`E41`TAAoe(;a#9V?|t zVUCYVZG8LakOgsnxy6n1GOF#}=l@Wd^#Tj+mU$%f<%o~%^9P)a=m(S>$dL7xknm2f zE3$Bkj$PSo>&0jOH=RK<3qvXDZz(cdzj@^ZN^~pCd0rWIj(P7Vd)sqO%nvHCc%w;lnG-_NOK9tLQ5fElQ z63{jFT_kLB@xq@Z`QqXikrC>@bVtiyHp{UuI^1OaV}nB@_IpVds(F`ys86mrta5zQBKGBu$b_ytdzrNNWMVraE^!qn+7!ApQ^RMp`JZn7?2h<400Z)xhkjhlV;^+~x=>HO(`YTr9)<0+ zynK@{xO=9P<)JCurG1E}y*OLu!Yj9eEX}{#VAT9naul6b6Z-7@_DvRrTiR0{9KpoL z=pRfWKNcJI`Mq!!-CJMoyz~yg(fMvgdgIngW?$I)#hrHq<}h_g1|I~;ah@^Cp};|@ zXTfUNTMKo6qLpvQv!fL#5SJ-Fal}cCdim4Oz9P{1+BA;M;X0lC<9xT(k=F6+>90b) zE&I|=Ca|4AgNwa1t>w_@K~&#xm-0@WaEM6t2Fcf-BEMsJHrCJjg5PRw@*P}p;}t)) zeaOK&4^I?So&%1fs0sZW7VK zE`~p-SLf}`li5(|d2tWoXD@h%sioEisX<#8$#{Ni*kNW{UTB#KLmA&ocPg_ca3~s! zj@YJN)}On|n)MK)KiL=Due%jDOSSOuD^$QumR7`r8w8%>8;IbiBun8K7TpTF#tfc$ zh$@Q1Cu{Ch~|#hQX$V@>P>x+p3p11oAjv zN2v2!$ch-(h^EB>+l}R*G@^9kz2Cohr(C@2yi&HXi1*&LDBB=>4{jj9k-#1z2ax0{ zjS=B|EC5k`F_6ErC;2WJiAu4`$Y8>g3RI53<4!Dqk_7gII5{mbL*Xc&H!(@CXu9rI zYaKw+%-*+6%4mWpe+F=)NRIh1VC4r18$^f+h)WCVi$u6VMa7ib=Uu+CMw4LTGUBb{ zw+0{lx#4E{=wSuKh?1fU64Y(LD(4*24YitRmP~m!*3#Yp)v>{y!33y4PKLe!%J5GS zfdNzK+Q4Qg7szxtMK?oGZFn<+x2z0gJE5{$x@#V+zUC>N7Dwb%&s5+#k6!B^(GI|`vvmu&7 z?N(NJO(^?TfJr+(n&O$X2GbNOk#E3C9v?@{QyD1s78+}pMD*OT`DJ7zi%t$1x=e&u zCBOp!pb=4lGQ0RhCKIr~h|NGZtZ((IB`3q-1gb5{+@-CJC7c^ic~s86U9C7qNoZ2E zi@lRSO4moeanbX51MX%>WO%7sLWc(~irgdtFr@Gu{WR}4NY)!-;mSto_#XZEb8(hE z@R_;SXS?Zx4~H24*|)Us{02zDxR<v`JDx``w0ZI8QRdeugOdTc3L3^j14$=XtX zb*pjG>0{TrjPD)Ri=b-pNK9-$BU<2=4=7-~n9I+*j zZI|`2V;Y!MZtuZ=#PLF{PQ~#XM5YQvPTVmWdhF<0^qW{4J2gLb2cr0HLD9G`yvCm( zo&l@hGw=PX3Jy{zw7ojiG1QTa4M9%^_UE=W{vH)NHa1lax#!hHTN%!R>M=@36V#F|s0jWp%Kt$fpM`Ao#XdS(`&6=hFo25yZnQ@x{n z0c6MN;3`o&flkB|gCgdWk2dzuBXuF|zj+hb1u3nMCQwOuVs6dOr$4Iz0OMr^x~Xbo zJ_6_Iw>Dc8Qh(w{yuP&{_=OOUfu=DhgBZAo-V?u&q*?$DOzZ&rZ~O0ql&6}^ncq&juy`h$DI1B44P>amlPnVRk$6fjFQ z#mPL~=!TX=kk^DYG@e0iZ4SS4t3_V7x*75*FJH0t)(Fj4JA)h`iXY+Cof#hP^b-P- zJ1``szRl!a^pFrTySUKK(3cV|QrockB>E`?72vQvS1IG6ZQn#ab^I`*Ee<&oE=p`U zC~b>jOA$MwPOJ`KtSdma42=DLK03xJvEIu4X?;CeqjEM$R$+8aJ`uoMq6Y=mV=P!- zKi#nWkVL{l!@|WHuZ48j{x}fdZmC#&-mIt5H<_nLr&J=68K~f+v*8zp4{T&UFed9j zsZQq#il{@GlhFm>ZQ8ej7P?{aePfSCc`>9TWh_ra=$9TTto&?zFW=Y70CsjWd zWO(p?q-E0i3+$#{Ce^~zho^OVYmQAAMuz*}nWI=nm{Qtk)0l}J(BXQq0`|$3je!9* zwEan8*(dFb{r>pl|LuRe4VKZrzS7bO4nzs|s$^aK{?#0MWwG_txqR*+)p}_pYDdzk z*)tPpK2aomorQE|(Izb8t zCOW31SvQDKY~?J)(TLLE@B2Eas`I8;cWXO^ak^CspK|rkHuu8fjzR8SF6q5hFn@-| zzR8i!As7C(T%sngl`dMqUcVAGO^avXrT171sTIlACuhlBeS{oZqi`uSR!3)Uv3IL0j=QRefEe zrOcfv07-SrvXbw1_aCw4`I^o6Gxzy3K{cJW-$n(4YekpgXVYpDX@kWg$4C+&A&jd` zCRsikn|*43uW$#CAZrZ2EmN9;HwDQTWx&YiT zISo}*^1`wgD4cqoPocQZQ1R z1Lh1!rc->07S^wlQ&Yit6bXKvv#!T{RuEt&MwA>Jo75oNVN}MaV5OfqUF7fh_OudN z-+3f(`n_-@SVc-gsKkJJy1s3PTW$N*8@wB2zdUXn0&o+(J#V00y(aT?V z!z=tY;Z%R=_f1V5dW&{eSkCiO{5p0O7&?!C*XtTCKa;$9AXlA+JYkn)+P51dh{%tp zWIU=_2E5WEX2xf>mRjGQ-4Y$u+-mqjLBxZGE-Es0=%Hegh*@4?P6`DcW9{PYmwkP& z)eTxda2oCEP_N29cA!p=S4j_^O1UyUvhQ+s_pY6fqlkyJIyEG@2e-&1 z!{4JvJ5jAzk4aWxK3xgHG)*K_RMb?KrRP2_2%d9hy{esSP!XAVRAhVk;J{S;?bS8h z$M;k>6D@UXll~smYoJ`e1!1WXWoi8U2-BhRU<_Xe_Eq)AikBlP@IO5N#UX6<^Pn0I zXvldN*jTT)w%zo5dC~eD_jdA}$B8AiyaFh93>61bdj$M|nSLvgNn3qG9*)j|iwZpD zdC5Z%F;O2JKi6M1=bWB*)5s(Pm%b5Y;}L&r|I7Z`tGw7Nk<)K9gh;CNlaQb5ij9(U zH0>iPQZ9-y=^S}xOj@wovH~%zL(FtLk(ej&VIP>fmLa|j<5Ir0&;1mNLuMg-jz#|nE6_pdyfBLSECAr5`*8y>8xYtvW z;y&|0#nLQjQIb}@$M`DU+Dc_-Z~gOTTHs>cOVbJ+9`Ctqr~OewCe;TfDJJ*4;YZYG zD~OP`cY`}!tJo`{CWa3i+)5}BsXxkGj=QDG5LrZo1xlG!&OVU5EVbOX| zVhUBIz3u&)O4hqRAl#?-=I4F-Y{?jfOg9=GBSMNU6m7O^1A1c7XGk1Q^YO#?HzGo9 zpY*kbVC;6vHn7cY_(i8@LgCqwPgvf-tN)N3dp=*je}0V2sf*ry9?v`1{~0xpIj~K5 zuv8xG?e7m}QtD@peg1@wMiFfO5blX^TYn8U%UoaJwsYJ!=1h*?e1FN#pQk-lFhEE1 zgIq7@A)CPMx{S2b`PReH;q}=fO7P5L(hCk#Bu=#Q{A=sdePWB8TRaPg7XMq@qHH!9 z(~(3<%J=b9KR=FDcsJPpHz$UAx5Vr5gn2@^NKV$^aZZIuLeUZR*-0qn##-=a`vvuc z4Y@$0tj%gK*7u86Z<|v{oNui30I>J@%QB36VllbH z`%5l)j9a`Yp3n@6bFgy)K1_YDT~PS2G#ED25+~(%Lb$hSVDcpsp61<1pYdP0hsehF zn(nZ?Pw1%4_tRT*XS{!(uv>$)wX5?Ks2{aFtJheG%KI9ai>7gPB!6eH`+Muzu=xQS z--?m3n0vuOq3`#=ZOU(`sr9xqJE8cUsb`MI2JX_eX6Zh9q6JK1;4M8Eatsh92Z3g( zrQ?QzDEy{ojCpZdI7isv_Lt>H($sr#Bl6lZx(9!%i)f`Xq`lsLBNw*JQ zHYWOeCNvt7GS)KD-se^@GV32Dd?@4;d+HWgh?u(CpywGme@^zo3Q`!328|8RZT|K7 zJMNxyWHedr;6d}E>1yD$R9&FhyQS;%2@jq~Q5sg*-z3l7%AGgEei7&e0U146Rf7K5 ztqs3|6>Mp;<&!#83zui=Pf5p3ot%|l3=Aw^ClVJ7_m*bYooPuLfBJWRpEKB*JmNE# z^hF-U>ZSVP{z^Ra{rfOr*KmokTSuA{ z&`=&8`xMbvRn|c%ePpCWlp#SLkR6gN2J)5~aC*kUNoN-vb*dR9mq`1oQyB{v zRTZOGMoY5Pu>zji^1khmy1ZRpPSGj)SFZT1G3}Wr@g(y;oL4xL(uQb2W#gTp(4w-x zGeKZxOyNHrR2u!>3E2?=yD}P~2Xh1AKLsRv{#k63Jb$rJ5fXu;q`J5`20Xpo7oZ0h zRw$bBn&-O~2Vpo$s~?crF|oGf0q9w)L_XQkFF4Skw5o^X(L{}D3*4$Fb> z3tV&3MLD7CY5=?bZ!Vsm*1SVI+g2V50iK8Mk4HP(rmftHbJgFgZ})CP5N}jURdU{A z_l8lIqN89eT1V?WFRTp7_sB64IVuX}Q!|^vBo%};+#ucDcyIcamkk$+-MLlVCxY{q zS3y5A)v~HH8svrl^Lm-=FF?^%aBIJyEb(yZUDc11M5LeS>T{l_fW(M(wYQt7P6-+y ztG5Iz+R4#*Z2%tcqWhMs3jv`wFyNEX%tF;EQd*$%(ND%I#+??`jtkCiTAV0kM#SgG$1=mC!jfX3+&uKfd;p(~#w5p+_96E_JUghmORY{Z z+Urow6HoPsqWGNnukiq4R@*xyryg+yu7IRAP=2& zg?`_UFE;}KW3FdL72gh0;G_-1hkye2fgcO&DT%>M&9*OhDL0n;y@-^^-)m8k^L66l zVU2c99vwKyL2)2EsAu8M!&8x+4!lnAui~LaSQ3=buP)km>BC*uz0Z;1NqX5M zBj#mu=$Z=KcngZecx*f#+zFZmDOQ|)3&>diAdC><>WslY9wg)jOlS#9((ot3?Z$|9 z?U^aj&vB770Oi>rS47wP(Ky12@Pv;P*aYNciD9Y8f?e8v0~A?_PjRmiTs8p}xTJBrc)^vNlq@@?x1xr3kipHi^O(o2ncXVDe@2&a9slYJeVO3|0&Xab#ao zQqilnF!dw^8II`Z{b{K=6n{#*>yg$1A(k$UnVk3uEIqx^j@RM7FG#7Y`R)FgNUU+g zRUu$Xv^H99cy~l?Jakb!m2*SM$je$1!mOw3rx#FV JKPn=^{}-y(^E3be diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 7cfcb3a443..4b8e9f0540 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -24,13 +24,19 @@ #group-page .group-logo, .group-header text-align: center + .group-logo + padding-bottom: 1em + max-height: 200px .group-name border-bottom: 1px solid #ccc @media screen and (min-width: 768px) .group-logo, .group-header text-align: left - .group-logo img - max-height: 100px + .group-logo + max-height: 120px + float: left + padding-right: 1em + background-color: white // Tabs .tabs dd a // Mobile first diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 5c6a6eddd9..0b66707871 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -5,15 +5,18 @@ .small-12.columns %img{"src" => @group.promo_image} .row - .small-12.medium-2.large-2.columns.group-logo.pad-top - %img{"src" => @group.logo} - .small-12.medium-10.large-10.columns.group-header.pad-top + / .small-6.medium-2.large-1.columns.group-logo.pad-top + + / .small-6.medium-10.large-11.columns.group-header.pad-top + + .small-12.columns.group-header.pad-top + %img.group-logo{"src" => @group.logo} %h2.group-name= @group.name %p= @group.description .small-12.columns.pad-top .row.pad-top - .small-12.medium-8.large-9.columns + .small-12.medium-12.large-9.columns %div{"ng-controller" => "TabsCtrl"} %tabset %tab{heading: 'Map', @@ -83,7 +86,7 @@ = render partial: 'shared/components/enterprise_no_results' - .small-12.medium-4.large-3.columns + .small-12.medium-12.large-3.columns = render partial: 'contact' / %h4 Contact us / - if @group.phone From 9e51b19f98e6690e4f0a3fcb085ca231748ef3ad Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Sat, 7 Feb 2015 18:11:51 +1100 Subject: [PATCH 623/681] right mapping of contributer's names in git --- .mailmap | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .mailmap diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000..0e268a507c --- /dev/null +++ b/.mailmap @@ -0,0 +1,2 @@ +Rob Harrington +Laura Summers From 2293623d2c72be358bc526b5c28d346aa41ce292 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Sat, 7 Feb 2015 19:24:36 +1100 Subject: [PATCH 624/681] Display default group logo if no logo was uploaded --- app/views/groups/show.html.haml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 0b66707871..fcf64a0892 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -3,14 +3,18 @@ %header .row .small-12.columns - %img{"src" => @group.promo_image} + - if @group.promo_image.present? + %img{"src" => @group.promo_image} .row / .small-6.medium-2.large-1.columns.group-logo.pad-top / .small-6.medium-10.large-11.columns.group-header.pad-top .small-12.columns.group-header.pad-top - %img.group-logo{"src" => @group.logo} + - if @group.logo.present? + %img.group-logo{"src" => @group.logo} + - else + %img.group-logo{"src" => '/assets/noimage/group.png'} %h2.group-name= @group.name %p= @group.description From 211e9c33a760b06521fb08bb86bd40be331c6eac Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Sat, 7 Feb 2015 19:31:16 +1100 Subject: [PATCH 625/681] Fixing link to group website --- app/helpers/groups_helper.rb | 6 ------ app/views/groups/_contact.html.haml | 3 +-- spec/helpers/groups_helper_spec.rb | 6 ------ 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index ba884ec8db..8b6ac9f5fd 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,11 +1,5 @@ module GroupsHelper - def link_to_url(url, html_options = {}) - link_to_service 'http://', url, html_options do - strip_url url - end - end - def link_to_service(baseurl, name, html_options = {}) if name.empty? then return end html_options = html_options.merge target: '_blank' diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index 8a612ae194..458aa81dbe 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -12,9 +12,8 @@ Email us - if @group.website.present? %p - %a{href:"http://"+@group.website, target: "_blank"} + =link_to_service "http://", @group.website do Visit our website - / =link_to_url @group.website %div{bindonce: true} - if @group.facebook.present? || @group.twitter.present? || @group.linkedin.present? || @group.instagram.present? diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 89b28ae3fa..69e7a3add4 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -14,10 +14,4 @@ describe GroupsHelper do expect(helper.strip_url("example.com")).to eq("example.com") end end - describe "link_to_url" do - it "gives a link to an html external url" do - expect(helper.link_to_url("example.com")).to eq('example.com') - expect(helper.link_to_url("https://example.com/")).to eq('example.com/') - end - end end From edcef020170a7dc422852b262871b73ae2bfac56 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Sat, 7 Feb 2015 19:40:59 +1100 Subject: [PATCH 626/681] link_to_service helper deals with nil --- app/helpers/groups_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 8b6ac9f5fd..6ca71a7b78 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,7 +1,7 @@ module GroupsHelper def link_to_service(baseurl, name, html_options = {}) - if name.empty? then return end + if name.blank? then return end html_options = html_options.merge target: '_blank' link_to ext_url(baseurl, name), html_options do yield From ca558a4ecce79d93a4aa09ec2b8314b67883e859 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 11 Feb 2015 11:55:48 +1100 Subject: [PATCH 627/681] Fix admin tab to check correct permission --- .../add_variant_overrides_tab.html.haml.deface | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface b/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface index 0f4dde1798..8db108b4f2 100644 --- a/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface +++ b/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface @@ -1,3 +1,3 @@ / insert_bottom "[data-hook='admin_product_sub_tabs']" -= tab :overrides, url: main_app.admin_variant_overrides_path, match_path: '/variant_overrides' += tab :variant_overrides, label: "Overrides", url: main_app.admin_variant_overrides_path, match_path: '/variant_overrides' From 46aee6f77c600d32c8760b6423cbedd683fe9cca Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 11 Feb 2015 14:30:49 +1100 Subject: [PATCH 628/681] Upgrade Rails to 3.2.21 --- Gemfile | 5 +-- Gemfile.lock | 89 +++++++++++++++++++++++----------------------------- 2 files changed, 42 insertions(+), 52 deletions(-) diff --git a/Gemfile b/Gemfile index 0a84a770c8..8998ac8882 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,13 @@ source 'https://rubygems.org' ruby "1.9.3" -gem 'rails', '3.2.19' +gem 'rails', '3.2.21' gem 'rails-i18n', '~> 3.0.0' +gem 'i18n', '~> 0.6.11' gem 'pg' gem 'spree', :github => 'openfoodfoundation/spree', :branch => '1-3-stable' -gem 'spree_i18n', :github => 'spree/spree_i18n' +gem 'spree_i18n', :github => 'spree/spree_i18n', :branch => '1-3-stable' gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '1-3-stable' # Waiting on merge of PR #117 diff --git a/Gemfile.lock b/Gemfile.lock index b365ba2952..57e0877ad2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,11 +86,13 @@ GIT GIT remote: git://github.com/spree/spree_i18n.git - revision: a96bee02340e008e60549295a4f09e047fd2e628 + revision: 752eb67204e9c5a4e22b62591a8fd55fe2285e43 + branch: 1-3-stable specs: spree_i18n (1.0.0) i18n (~> 0.5) - spree (~> 1.1) + rails-i18n + spree_core (>= 1.1) GIT remote: git://github.com/willrjmarshall/foundation_rails_helper.git @@ -105,12 +107,12 @@ GIT GEM remote: https://rubygems.org/ specs: - actionmailer (3.2.19) - actionpack (= 3.2.19) + actionmailer (3.2.21) + actionpack (= 3.2.21) mail (~> 2.5.4) - actionpack (3.2.19) - activemodel (= 3.2.19) - activesupport (= 3.2.19) + actionpack (3.2.21) + activemodel (= 3.2.21) + activesupport (= 3.2.21) builder (~> 3.0.0) erubis (~> 2.7.0) journey (~> 1.0.4) @@ -121,29 +123,23 @@ GEM active_link_to (1.0.0) active_model_serializers (0.8.1) activemodel (>= 3.0) - active_utils (2.2.3) - activesupport (>= 2.3.11) - i18n - activemerchant (1.44.1) - active_utils (~> 2.2.0) + activemerchant (1.46.0) activesupport (>= 3.2.14, < 5.0.0) builder (>= 2.1.2, < 4.0.0) i18n (>= 0.6.9) - json (~> 1.7) nokogiri (~> 1.4) - offsite_payments (~> 2.0.0) - activemodel (3.2.19) - activesupport (= 3.2.19) + activemodel (3.2.21) + activesupport (= 3.2.21) builder (~> 3.0.0) - activerecord (3.2.19) - activemodel (= 3.2.19) - activesupport (= 3.2.19) + activerecord (3.2.21) + activemodel (= 3.2.21) + activesupport (= 3.2.21) arel (~> 3.0.2) tzinfo (~> 0.3.29) - activeresource (3.2.19) - activemodel (= 3.2.19) - activesupport (= 3.2.19) - activesupport (3.2.19) + activeresource (3.2.21) + activemodel (= 3.2.21) + activesupport (= 3.2.21) + activesupport (3.2.21) i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) acts_as_list (0.1.4) @@ -185,7 +181,7 @@ GEM climate_control (0.0.3) activesupport (>= 3.0) cliver (0.3.2) - cocaine (0.5.4) + cocaine (0.5.5) climate_control (>= 0.0.3, < 1.0) coderay (1.0.9) coffee-rails (3.2.2) @@ -195,7 +191,7 @@ GEM coffee-script-source execjs coffee-script-source (1.3.3) - colorize (0.7.3) + colorize (0.7.5) columnize (0.3.6) comfortable_mexican_sofa (1.6.24) active_link_to (~> 1.0.0) @@ -308,7 +304,7 @@ GEM jquery-rails (2.2.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - json (1.8.1) + json (1.8.2) json_spec (1.1.1) multi_json (~> 1.0) rspec (~> 2.0) @@ -331,7 +327,7 @@ GEM treetop (~> 1.4.8) method_source (0.8.1) mime-types (1.25.1) - mini_portile (0.6.1) + mini_portile (0.6.2) momentjs-rails (2.5.1) railties (>= 3.1) money (5.1.1) @@ -342,16 +338,8 @@ GEM net-ssh (>= 2.6.5) net-ssh (2.6.8) newrelic_rpm (3.6.7.152) - nokogiri (1.6.4.1) + nokogiri (1.6.6.2) mini_portile (~> 0.6.0) - offsite_payments (2.0.1) - active_utils (~> 2.2.0) - activesupport (>= 3.2.14, < 5.0.0) - builder (>= 2.1.2, < 4.0.0) - i18n (~> 0.5) - json (~> 1.7) - money (< 7.0.0) - nokogiri (~> 1.4) oj (2.1.2) orm_adapter (0.5.0) paperclip (3.5.4) @@ -390,28 +378,28 @@ GEM rack rack-ssl (1.3.4) rack - rack-test (0.6.2) + rack-test (0.6.3) rack (>= 1.0) - rails (3.2.19) - actionmailer (= 3.2.19) - actionpack (= 3.2.19) - activerecord (= 3.2.19) - activeresource (= 3.2.19) - activesupport (= 3.2.19) + rails (3.2.21) + actionmailer (= 3.2.21) + actionpack (= 3.2.21) + activerecord (= 3.2.21) + activeresource (= 3.2.21) + activesupport (= 3.2.21) bundler (~> 1.0) - railties (= 3.2.19) + railties (= 3.2.21) rails-i18n (3.0.1) i18n (~> 0.5) rails (>= 3.0.0, < 4.0.0) - railties (3.2.19) - actionpack (= 3.2.19) - activesupport (= 3.2.19) + railties (3.2.21) + actionpack (= 3.2.21) + activesupport (= 3.2.21) rack-ssl (~> 1.3.2) rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) raindrops (0.9.0) - rake (10.3.2) + rake (10.4.2) ransack (0.7.2) actionpack (~> 3.0) activerecord (~> 3.0) @@ -493,7 +481,7 @@ GEM sprockets (>= 2.0.0) turn (0.8.3) ansi - tzinfo (0.3.42) + tzinfo (0.3.43) uglifier (1.2.4) execjs (>= 0.3.0) multi_json (>= 1.0.2) @@ -561,6 +549,7 @@ DEPENDENCIES guard-rspec guard-zeus haml + i18n (~> 0.6.11) immigrant jquery-rails json_spec @@ -575,7 +564,7 @@ DEPENDENCIES rabl rack-livereload rack-ssl - rails (= 3.2.19) + rails (= 3.2.21) rails-i18n (~> 3.0.0) representative_view roadie-rails (~> 1.0.3) From 0fdf313424fd9812af6b645501b71c707221f20f Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 12 Feb 2015 10:31:13 +1100 Subject: [PATCH 629/681] Fixing img display of producers if no logo present --- app/views/producers/_fat.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml index f420b33386..17304abbe3 100644 --- a/app/views/producers/_fat.html.haml +++ b/app/views/producers/_fat.html.haml @@ -4,7 +4,7 @@ / Will add in long description available once clean up HTML formatting producer.long_description %div{"bo-if" => "producer.description"} %label About us - %img.right.show-for-medium-up{src: "{{ producer.logo }}" } + %img.right.show-for-medium-up{"ng-src" => "{{producer.logo}}" } %p.text-small {{ producer.description }} %div.show-for-medium-up{"bo-if" => "producer.description.length==0"} From 773a5480e904e48420496ea71510255e2a67e91d Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 12 Feb 2015 10:44:20 +1100 Subject: [PATCH 630/681] Activate search input for groups again --- app/views/groups/index.html.haml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 49bd277532..1688446a7f 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -8,11 +8,11 @@ .small-12.columns %h1 Groups / regions %p - / %input.animate-show{type: :text, - / "ng-model" => "query", - / placeholder: "Search name or keyword", - / "ng-debounce" => "150", - / "ofn-disable-enter" => true} + %input.animate-show{type: :text, + "ng-model" => "query", + placeholder: "Search name or keyword", + "ng-debounce" => "150", + "ofn-disable-enter" => true} .group{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", name: "group{{group.id}}", From 57e838898fa30427f5d0ee1f191df1b0bd082cc9 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 12 Feb 2015 11:05:21 +1100 Subject: [PATCH 631/681] Update documentation of database setup db:setup is doing db:schema and db:seed already --- README.markdown | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.markdown b/README.markdown index 8c9883dc04..5448e34328 100644 --- a/README.markdown +++ b/README.markdown @@ -53,14 +53,11 @@ Configure the site: cp config/application.yml.example config/application.yml edit config/application.yml -Create the development and test databases, using the settings specified in `config/database.yml`: +Create the development and test databases, using the settings specified in `config/database.yml` +and load data from `db/seeds.rb`: rake db:setup -Then load the schema and some seed data with the following command: - - rake db:schema:load db:seed - Load some default data for your environment: rake openfoodnetwork:dev:load_sample_data @@ -99,4 +96,4 @@ usage instructions. ## Licence -Copyright (c) 2012 - 2013 Open Food Foundation, released under the AGPL licence. +Copyright (c) 2012 - 2015 Open Food Foundation, released under the AGPL licence. From dde1a27d3a05d1b1f241a2411876f38941a0ef36 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 30 Jan 2015 12:47:39 +1100 Subject: [PATCH 632/681] Adding ability to search for known users --- app/models/spree/ability_decorator.rb | 2 ++ spec/models/spree/ability_spec.rb | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 370b219eff..eb8dcbd60d 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -63,6 +63,8 @@ class AbilityDecorator can [:admin, :read, :edit, :bulk_update, :destroy], EnterpriseFee do |enterprise_fee| user.enterprises.include? enterprise_fee.enterprise end + + can [:admin, :known_users], :search end def add_product_management_abilities(user) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 4ffcc0e3fd..30fbbd8ab6 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -410,6 +410,11 @@ module Spree it 'should have the ability administrate and create enterpises' do should have_ability([:admin, :index, :create], for: Enterprise) end + + it "should have the ability to search for users which share management of its enterprises" do + should have_ability([:admin, :known_users], for: :search) + should_not have_ability([:users], for: :search) + end end end end From b73619d168fa0902aa3940d78666a74b3a5037d8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 30 Jan 2015 12:48:21 +1100 Subject: [PATCH 633/681] Adding known_users method to Spree::User --- app/models/spree/user_decorator.rb | 6 ++++++ spec/models/spree/user_spec.rb | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 4f119ab81b..1a41fc5b48 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -11,6 +11,12 @@ Spree.user_class.class_eval do validate :limit_owned_enterprises + def known_users + Spree::User + .includes(:enterprises) + .where("enterprises.id IN (SELECT enterprise_id FROM enterprise_roles WHERE user_id = ?)", id) + end + def build_enterprise_roles Enterprise.all.each do |enterprise| unless self.enterprise_roles.find_by_enterprise_id enterprise.id diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index e1a9239c95..204b6fe9c5 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -30,4 +30,22 @@ describe Spree.user_class do create(:user) end end + + describe "known_users" do + let(:u1) { create(:user) } + let(:u2) { create(:user) } + let(:u3) { create(:user) } + let!(:e1) { create(:enterprise, owner: u1, users: [u1, u2]) } + + it "returns a list of users which manage shared enterprises" do + t1 = Time.now + expect(u1.known_users).to include u1, u2 + expect(u1.known_users).to_not include u3 + expect(u2.known_users).to include u1,u2 + expect(u2.known_users).to_not include u3 + expect(u3.known_users).to_not include u1,u2,u3 + t2 = Time.now + binding.pry + end + end end From dcb24cf06c122ee475d5e99e39c6dd454b064a7a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 30 Jan 2015 12:49:00 +1100 Subject: [PATCH 634/681] Controller method to allow enterprise users to search for other users which share management of their enterprises --- .../admin/search_controller_decorator.rb | 19 ++++++++++ .../spree/admin/search_controller_spec.rb | 35 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 app/controllers/spree/admin/search_controller_decorator.rb create mode 100644 spec/controllers/spree/admin/search_controller_spec.rb diff --git a/app/controllers/spree/admin/search_controller_decorator.rb b/app/controllers/spree/admin/search_controller_decorator.rb new file mode 100644 index 0000000000..13fb62d467 --- /dev/null +++ b/app/controllers/spree/admin/search_controller_decorator.rb @@ -0,0 +1,19 @@ +Spree::Admin::SearchController.class_eval do + def known_users + binding.pry + if exact_match = Spree.user_class.find_by_email(params[:q]) + @users = [exact_match] + else + @users = spree_current_user.known_users.ransack({ + :m => 'or', + :email_start => params[:q], + :ship_address_firstname_start => params[:q], + :ship_address_lastname_start => params[:q], + :bill_address_firstname_start => params[:q], + :bill_address_lastname_start => params[:q] + }).result.limit(10) + end + render :users + + end +end diff --git a/spec/controllers/spree/admin/search_controller_spec.rb b/spec/controllers/spree/admin/search_controller_spec.rb new file mode 100644 index 0000000000..b66ba94888 --- /dev/null +++ b/spec/controllers/spree/admin/search_controller_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Spree::Admin::SearchController do + include AuthenticationWorkflow + context "Distributor Enterprise User" do + let!(:owner) { create_enterprise_user( email: "test1@email.com" ) } + let!(:manager) { create_enterprise_user( email: "test2@email.com" ) } + let!(:random) { create_enterprise_user( email: "test3@email.com" ) } + let!(:enterprise) { create(:enterprise, owner: owner, users: [owner, manager]) } + before { login_as_enterprise_user [enterprise] } + + describe 'searching for known users' do + describe "when search query is not an exact match" do + before do + spree_get :known_users, q: "test" + end + + it "returns a list of users that I share management of enteprises with" do + expect(assigns(:users)).to include owner, manager + expect(assigns(:users)).to_not include random + end + end + + describe "when search query exactly matches the email of a user in the system" do + before do + spree_get :known_users, q: "test3@email.com" + end + + it "returns that user, regardless of the relationship between the two users" do + expect(assigns(:users)).to eq [random] + end + end + end + end +end From ad1f837c44e4f82993ee25cd37b016806e6b1837 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 30 Jan 2015 14:03:11 +1100 Subject: [PATCH 635/681] Admin users can access all users through known_users --- .../admin/search_controller_decorator.rb | 1 - app/models/spree/user_decorator.rb | 10 ++++-- spec/models/spree/user_spec.rb | 32 ++++++++++++------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/controllers/spree/admin/search_controller_decorator.rb b/app/controllers/spree/admin/search_controller_decorator.rb index 13fb62d467..f268f95820 100644 --- a/app/controllers/spree/admin/search_controller_decorator.rb +++ b/app/controllers/spree/admin/search_controller_decorator.rb @@ -1,6 +1,5 @@ Spree::Admin::SearchController.class_eval do def known_users - binding.pry if exact_match = Spree.user_class.find_by_email(params[:q]) @users = [exact_match] else diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 1a41fc5b48..1bafc85d0c 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -12,9 +12,13 @@ Spree.user_class.class_eval do validate :limit_owned_enterprises def known_users - Spree::User - .includes(:enterprises) - .where("enterprises.id IN (SELECT enterprise_id FROM enterprise_roles WHERE user_id = ?)", id) + if admin? + Spree::User.all + else + Spree::User + .includes(:enterprises) + .where("enterprises.id IN (SELECT enterprise_id FROM enterprise_roles WHERE user_id = ?)", id) + end end def build_enterprise_roles diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 204b6fe9c5..c739b4378b 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -1,4 +1,6 @@ describe Spree.user_class do + include AuthenticationWorkflow + describe "associations" do it { should have_many(:owned_enterprises) } @@ -32,20 +34,26 @@ describe Spree.user_class do end describe "known_users" do - let(:u1) { create(:user) } - let(:u2) { create(:user) } - let(:u3) { create(:user) } + let!(:u1) { create_enterprise_user } + let!(:u2) { create_enterprise_user } + let!(:u3) { create_enterprise_user } let!(:e1) { create(:enterprise, owner: u1, users: [u1, u2]) } - it "returns a list of users which manage shared enterprises" do - t1 = Time.now - expect(u1.known_users).to include u1, u2 - expect(u1.known_users).to_not include u3 - expect(u2.known_users).to include u1,u2 - expect(u2.known_users).to_not include u3 - expect(u3.known_users).to_not include u1,u2,u3 - t2 = Time.now - binding.pry + describe "as an enterprise user" do + it "returns a list of users which manage shared enterprises" do + expect(u1.known_users).to include u1, u2 + expect(u1.known_users).to_not include u3 + expect(u2.known_users).to include u1,u2 + expect(u2.known_users).to_not include u3 + expect(u3.known_users).to_not include u1,u2,u3 + end + end + + describe "as admin" do + let(:admin) { quick_login_as_admin } + it "returns all users" do + expect(admin.known_users).to include u1, u2, u3 + end end end end From ee5ab225010b9a556d24c82f6beed715f6294801 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 30 Jan 2015 15:19:08 +1100 Subject: [PATCH 636/681] Moving interface for changing owner to new 'Users' tab --- .../controllers/enterprise_controller.js.coffee | 1 - .../controllers/side_menu_controller.js.coffee | 4 ++-- .../users/directives/user_autocomplete.js.coffee | 4 ++-- app/models/spree/user_decorator.rb | 2 +- app/views/admin/enterprises/_form.html.haml | 4 ++++ .../enterprises/form/_primary_details.html.haml | 12 +----------- app/views/admin/enterprises/form/_users.html.haml | 15 +++++++++++++++ config/routes.rb | 2 ++ spec/features/admin/enterprises_spec.rb | 4 +++- 9 files changed, 30 insertions(+), 18 deletions(-) create mode 100644 app/views/admin/enterprises/form/_users.html.haml diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index a9a6f2dd2d..c96c0cc2a6 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,7 +1,6 @@ angular.module("admin.enterprises") .controller "enterpriseCtrl", ($scope, NavigationCheck, Enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu) -> $scope.Enterprise = Enterprise.enterprise - console.log Enterprise $scope.PaymentMethods = EnterprisePaymentMethods.paymentMethods $scope.ShippingMethods = EnterpriseShippingMethods.shippingMethods $scope.navClear = NavigationCheck.clear diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index 85a15ebdf3..a74c3005e0 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -5,7 +5,8 @@ angular.module("admin.enterprises") $scope.select = SideMenu.select $scope.menu.setItems [ - { name: 'Primary Details', icon_class: "icon-user" } + { name: 'Primary Details', icon_class: "icon-home" } + { name: 'Users', icon_class: "icon-user" } { name: 'Address', icon_class: "icon-map-marker" } { name: 'Contact', icon_class: "icon-phone" } { name: 'Social', icon_class: "icon-twitter" } @@ -38,4 +39,3 @@ angular.module("admin.enterprises") $scope.showShopPreferences = -> $scope.Enterprise.sells != "none" - diff --git a/app/assets/javascripts/admin/users/directives/user_autocomplete.js.coffee b/app/assets/javascripts/admin/users/directives/user_autocomplete.js.coffee index a904d1bcce..2c2f2a12fd 100644 --- a/app/assets/javascripts/admin/users/directives/user_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/users/directives/user_autocomplete.js.coffee @@ -6,7 +6,7 @@ angular.module("admin.users").directive "ofnUserAutocomplete", ($http) -> initSelection: (element, callback) -> callback { id: element.val(), email: attrs.email } ajax: - url: Spree.routes.user_search + url: '/admin/search/known_users' datatype: 'json' data:(term, page) -> { q: term } @@ -15,4 +15,4 @@ angular.module("admin.users").directive "ofnUserAutocomplete", ($http) -> formatResult: (user) -> user.email formatSelection: (user) -> - user.email \ No newline at end of file + user.email diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 1bafc85d0c..c57e709cf9 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -13,7 +13,7 @@ Spree.user_class.class_eval do def known_users if admin? - Spree::User.all + Spree::User else Spree::User .includes(:enterprises) diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index d63ae9c143..aa2ae7f589 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -2,6 +2,10 @@ %legend Primary Details = render 'admin/enterprises/form/primary_details', f: f +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Users'" } } + %legend Users + = render 'admin/enterprises/form/users', f: f + = f.fields_for :address do |af| %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Address'" } } %legend Address diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml index 243f6076fb..dce5512f40 100644 --- a/app/views/admin/enterprises/form/_primary_details.html.haml +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -14,16 +14,6 @@ .eight.columns.omega = f.collection_select :group_ids, EnterpriseGroup.all, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..." -- if spree_current_user.admin? - .row - .three.columns.alpha - =f.label :owner_id, 'Owner' - %span.required * - .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} - %a What's this? - .eight.columns - - owner_email = @enterprise.andand.owner.andand.email || "" - = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email .row .three.columns.alpha @@ -86,4 +76,4 @@ %a What's this? .eight.columns.omega = surround spree.root_url, "/shop" do - {{Enterprise.permalink}} \ No newline at end of file + {{Enterprise.permalink}} diff --git a/app/views/admin/enterprises/form/_users.html.haml b/app/views/admin/enterprises/form/_users.html.haml new file mode 100644 index 0000000000..92ad0c1a90 --- /dev/null +++ b/app/views/admin/enterprises/form/_users.html.haml @@ -0,0 +1,15 @@ +- owner_email = @enterprise.andand.owner.andand.email || "" +- full_permissions = (spree_current_user.admin? || spree_current_user == @enterprise.andand.owner) + +.row + .three.columns.alpha + =f.label :owner_id, 'Owner' + - if full_permissions + %span.required * + .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} + %a What's this? + .eight.columns.omega + - if full_permissions + = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email + - else + = owner_email diff --git a/config/routes.rb b/config/routes.rb index 7b88f6014a..e7f9f2bba0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -157,6 +157,8 @@ Spree::Core::Engine.routes.prepend do end namespace :admin do + get '/search/known_users' => "search#known_users", :as => :search_known_users + resources :products do get :product_distributions, on: :member diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 836a408bf3..0bd9b8dfb5 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -125,12 +125,12 @@ feature %q{ # Filling in details fill_in 'enterprise_name', :with => 'Eaterprises' + # This call intermittently fails to complete, leaving the select2 box open obscuring the # fields below it (which breaks the remainder of our specs). Calling it twice seems to # solve the problem. select2_search admin.email, from: 'Owner' select2_search admin.email, from: 'Owner' - choose 'Any' fill_in 'enterprise_contact', :with => 'Kirsten or Ren' fill_in 'enterprise_phone', :with => '0413 897 321' @@ -166,6 +166,8 @@ feature %q{ fill_in 'enterprise_name', :with => 'Eaterprises' choose 'Own' + + within (".side_menu") { click_link "Users" } select2_search user.email, from: 'Owner' click_link "About" From 5c09ebf138ba28a0967d1c840752036caf216c63 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 11 Feb 2015 14:03:04 +1100 Subject: [PATCH 637/681] Adding manager managment to enterprise edit form --- .../enterprise_controller.js.coffee | 16 +++++++ ...mplete.js.coffee => user_select.js.coffee} | 9 +++- .../api/admin/enterprise_serializer.rb | 6 ++- .../admin/enterprises/_new_form.html.haml | 4 +- .../admin/enterprises/form/_users.html.haml | 29 ++++++++++- .../enterprise_controller_spec.js.coffee | 48 +++++++++++++++++++ 6 files changed, 106 insertions(+), 6 deletions(-) rename app/assets/javascripts/admin/users/directives/{user_autocomplete.js.coffee => user_select.js.coffee} (64%) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index c96c0cc2a6..03cd7d4943 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -6,6 +6,7 @@ angular.module("admin.enterprises") $scope.navClear = NavigationCheck.clear $scope.pristineEmail = $scope.Enterprise.email $scope.menu = SideMenu + $scope.newManager = { id: '', email: 'Add a manager...' } # Provide a callback for generating warning messages displayed before leaving the page. This is passed in # from a directive "nav-check" in the page - if we pass it here it will be called in the test suite, @@ -16,3 +17,18 @@ angular.module("admin.enterprises") # Register the NavigationCheck callback NavigationCheck.register(enterpriseNavCallback) + + $scope.removeManager = (manager) -> + if manager.id? + for i, user of $scope.Enterprise.users when user.id == manager.id + $scope.Enterprise.users.splice i, 1 + + $scope.addManager = (manager) -> + if manager.id? and manager.email? + manager = + id: manager.id + email: manager.email + if (user for user in $scope.Enterprise.users when user.id == manager.id).length == 0 + $scope.Enterprise.users.push manager + else + alert "#{manager.email} is already a manager!" diff --git a/app/assets/javascripts/admin/users/directives/user_autocomplete.js.coffee b/app/assets/javascripts/admin/users/directives/user_select.js.coffee similarity index 64% rename from app/assets/javascripts/admin/users/directives/user_autocomplete.js.coffee rename to app/assets/javascripts/admin/users/directives/user_select.js.coffee index 2c2f2a12fd..94df1894d9 100644 --- a/app/assets/javascripts/admin/users/directives/user_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/users/directives/user_select.js.coffee @@ -1,10 +1,13 @@ -angular.module("admin.users").directive "ofnUserAutocomplete", ($http) -> +angular.module("admin.users").directive "userSelect", -> + scope: + user: '&userSelect' + model: '=ngModel' link: (scope,element,attrs) -> setTimeout -> element.select2 multiple: false initSelection: (element, callback) -> - callback { id: element.val(), email: attrs.email } + callback {id: scope.user().id, email: scope.user().email} ajax: url: '/admin/search/known_users' datatype: 'json' @@ -15,4 +18,6 @@ angular.module("admin.users").directive "ofnUserAutocomplete", ($http) -> formatResult: (user) -> user.email formatSelection: (user) -> + scope.$apply -> + scope.model = user if scope.model? user.email diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index d8b3a54bc2..c686aa9f69 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -2,4 +2,8 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids attributes :producer_profile_only, :email, :long_description, :permalink attributes :preferred_shopfront_message, :preferred_shopfront_closed_message, :preferred_shopfront_taxon_order -end \ No newline at end of file + attributes :owner, :users + + has_one :owner, serializer: Api::Admin::UserSerializer + has_many :users, serializer: Api::Admin::UserSerializer +end diff --git a/app/views/admin/enterprises/_new_form.html.haml b/app/views/admin/enterprises/_new_form.html.haml index bb43a421fb..a76c8ec31c 100644 --- a/app/views/admin/enterprises/_new_form.html.haml +++ b/app/views/admin/enterprises/_new_form.html.haml @@ -12,7 +12,7 @@ %a What's this? .nine.columns.omega - owner_email = @enterprise.andand.owner.andand.email || "" - = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email + = f.hidden_field :owner_id, class: "select2 fullwidth", 'user-select' => 'Enterprise.owner' .row .three.columns.alpha %label Primary Producer? @@ -102,4 +102,4 @@   .row .twelve.columns.alpha - = render partial: "spree/admin/shared/new_resource_links" \ No newline at end of file + = render partial: "spree/admin/shared/new_resource_links" diff --git a/app/views/admin/enterprises/form/_users.html.haml b/app/views/admin/enterprises/form/_users.html.haml index 92ad0c1a90..d13723795d 100644 --- a/app/views/admin/enterprises/form/_users.html.haml +++ b/app/views/admin/enterprises/form/_users.html.haml @@ -10,6 +10,33 @@ %a What's this? .eight.columns.omega - if full_permissions - = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email + = f.hidden_field :owner_id, class: "select2 fullwidth", 'user-select' => 'Enterprise.owner' - else = owner_email + +.row + .three.columns.alpha + =f.label :user_ids, 'Managers' + - if full_permissions + %span.required * + .with-tip{'data-powertip' => "The other users with permission to manage this enterprise."} + %a What's this? + .eight.columns.omega + - if full_permissions + %table + %tr + %td + - # Ignore this input in the submit + = hidden_field_tag :ignored, :new_manager, class: "select2 fullwidth", 'user-select' => 'newManager', 'ng-model' => 'newManager' + %td.actions + %a{ 'ng-click' => 'addManager(newManager)', :class => "icon-plus no-text" } + %tr.animate-repeat{ ng: { repeat: 'manager in Enterprise.users' }} + %td + = hidden_field_tag "enterprise[user_ids][]", nil, multiple: true, 'ng-value' => 'manager.id' + {{ manager.email }} + %td.actions + %a{ 'ng-click' => 'removeManager(manager)', :class => "icon-trash no-text" } + - else + - @enterprise.users.each do |manager| + = manager.email + %br diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee index 19fc22cb65..71e4c37aee 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee @@ -29,3 +29,51 @@ describe "enterpriseCtrl", -> it "stores shipping methods", -> expect(scope.ShippingMethods).toBe ShippingMethods.shippingMethods + + describe "adding managers", -> + u1 = u2 = u3 = null + beforeEach -> + u1 = { id: 1, email: 'name1@email.com' } + u2 = { id: 2, email: 'name2@email.com' } + u3 = { id: 3, email: 'name3@email.com' } + Enterprise.enterprise.users = [u1, u2 ,u3] + + it "adds a user to the list", -> + u4 = { id: 4, email: "name4@email.com" } + scope.addManager u4 + expect(Enterprise.enterprise.users).toContain u4 + + it "ignores object without an id", -> + u4 = { not_id: 4, email: "name4@email.com" } + scope.addManager u4 + expect(Enterprise.enterprise.users).not.toContain u4 + + it "it ignores objects without an email", -> + u4 = { id: 4, not_email: "name4@email.com" } + scope.addManager u4 + expect(Enterprise.enterprise.users).not.toContain u4 + + it "ignores objects that are already in the list, and alerts the user", -> + spyOn(window, "alert").andCallThrough() + u4 = { id: 3, email: "email-doesn't-matter.com" } + scope.addManager u4 + expect(Enterprise.enterprise.users).not.toContain u4 + expect(window.alert).toHaveBeenCalledWith "email-doesn't-matter.com is already a manager!" + + + describe "removing managers", -> + u1 = u2 = u3 = null + beforeEach -> + u1 = { id: 1, email: 'name1@email.com' } + u2 = { id: 2, email: 'name2@email.com' } + u3 = { id: 3, email: 'name3@email.com' } + Enterprise.enterprise.users = [u1, u2 ,u3] + + + it "removes a user with the given id", -> + scope.removeManager {id: 2} + expect(Enterprise.enterprise.users).not.toContain u2 + + it "does nothing when given object has no id attribute", -> + scope.removeManager {not_id: 2} + expect(Enterprise.enterprise.users).toEqual [u1,u2,u3] From a62f48441deac804c8833f9aab0c057bcccd7e78 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 11 Feb 2015 15:31:16 +1100 Subject: [PATCH 638/681] Restrict ability to change managers of enterprises using update action --- .../admin/enterprises_controller.rb | 7 + .../admin/enterprises_controller_spec.rb | 123 +++++++++++------- 2 files changed, 84 insertions(+), 46 deletions(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 9232144c21..503fd736ba 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -9,6 +9,7 @@ module Admin before_filter :override_owner, only: :create before_filter :check_can_change_owner, only: :update before_filter :check_can_change_bulk_owner, only: :bulk_update + before_filter :check_can_change_managers, only: :update helper 'spree/products' include OrderCyclesHelper @@ -130,6 +131,12 @@ module Admin end end + def check_can_change_managers + unless ( spree_current_user == @enterprise.owner ) || spree_current_user.admin? + params[:enterprise].delete :user_ids + end + end + # Overriding method on Spree's resource controller def location_after_save if params[:enterprise].key? :producer_properties_attributes diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 6b29d00107..5b3b182842 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -3,36 +3,39 @@ require 'spec_helper' module Admin describe EnterprisesController do include AuthenticationWorkflow - let(:distributor_owner) do - user = create(:user) - user.spree_roles = [] - user - end - let(:distributor) { create(:distributor_enterprise, owner: distributor_owner ) } - let(:user) do + let(:user) { create_enterprise_user } + let(:distributor_manager) do user = create(:user) user.spree_roles = [] distributor.enterprise_roles.build(user: user).save user end + let(:distributor_owner) do + user = create(:user) + user.spree_roles = [] + user + end let(:admin_user) do user = create(:user) user.spree_roles << Spree::Role.find_or_create_by_name!('admin') user end + let(:distributor) { create(:distributor_enterprise, owner: distributor_owner ) } + + describe "creating an enterprise" do let(:country) { Spree::Country.find_by_name 'Australia' } let(:state) { Spree::State.find_by_name 'Victoria' } let(:enterprise_params) { {enterprise: {name: 'zzz', permalink: 'zzz', email: "bob@example.com", address_attributes: {address1: 'a', city: 'a', zipcode: 'a', country_id: country.id, state_id: state.id}}} } it "grants management permission if the current user is an enterprise user" do - controller.stub spree_current_user: user - enterprise_params[:enterprise][:owner_id] = user + controller.stub spree_current_user: distributor_manager + enterprise_params[:enterprise][:owner_id] = distributor_manager spree_put :create, enterprise_params enterprise = Enterprise.find_by_name 'zzz' - user.enterprise_roles.where(enterprise_id: enterprise).first.should be + distributor_manager.enterprise_roles.where(enterprise_id: enterprise).first.should be end it "does not grant management permission to admins" do @@ -45,41 +48,12 @@ module Admin end it "it overrides the owner_id submitted by the user unless current_user is super admin" do - controller.stub spree_current_user: user - enterprise_params[:enterprise][:owner_id] = admin_user + controller.stub spree_current_user: distributor_manager + enterprise_params[:enterprise][:owner_id] = user spree_put :create, enterprise_params enterprise = Enterprise.find_by_name 'zzz' - user.enterprise_roles.where(enterprise_id: enterprise).first.should be - end - end - - describe "updating an enterprise" do - it "allows current owner to change ownership" do - controller.stub spree_current_user: distributor_owner - update_params = { id: distributor, enterprise: { owner_id: user } } - spree_post :update, update_params - - distributor.reload - expect(distributor.owner).to eq user - end - - it "allows super admin to change ownership" do - controller.stub spree_current_user: admin_user - update_params = { id: distributor, enterprise: { owner_id: user } } - spree_post :update, update_params - - distributor.reload - expect(distributor.owner).to eq user - end - - it "does not allow managers to change ownership" do - controller.stub spree_current_user: user - update_params = { id: distributor, enterprise: { owner_id: user } } - spree_post :update, update_params - - distributor.reload - expect(distributor.owner).to eq distributor_owner + distributor_manager.enterprise_roles.where(enterprise_id: enterprise).first.should be end end @@ -88,14 +62,52 @@ module Admin context "as manager" do it "does not allow 'sells' to be changed" do - profile_enterprise.enterprise_roles.build(user: user).save - controller.stub spree_current_user: user + profile_enterprise.enterprise_roles.build(user: distributor_manager).save + controller.stub spree_current_user: distributor_manager enterprise_params = { id: profile_enterprise, enterprise: { sells: 'any' } } spree_put :update, enterprise_params profile_enterprise.reload expect(profile_enterprise.sells).to eq 'none' end + + it "does not allow owner to be changed" do + controller.stub spree_current_user: distributor_manager + update_params = { id: distributor, enterprise: { owner_id: distributor_manager } } + spree_post :update, update_params + + distributor.reload + expect(distributor.owner).to eq distributor_owner + end + + it "does not allow managers to be changed" do + controller.stub spree_current_user: distributor_manager + update_params = { id: distributor, enterprise: { user_ids: [distributor_owner.id,distributor_manager.id,user.id] } } + spree_post :update, update_params + + distributor.reload + expect(distributor.users).to_not include user + end + end + + context "as owner" do + it "allows owner to be changed" do + controller.stub spree_current_user: distributor_owner + update_params = { id: distributor, enterprise: { owner_id: distributor_manager } } + spree_post :update, update_params + + distributor.reload + expect(distributor.owner).to eq distributor_manager + end + + it "allows managers to be changed" do + controller.stub spree_current_user: distributor_owner + update_params = { id: distributor, enterprise: { user_ids: [distributor_owner.id,distributor_manager.id,user.id] } } + spree_post :update, update_params + + distributor.reload + expect(distributor.users).to include user + end end context "as super admin" do @@ -107,6 +119,25 @@ module Admin profile_enterprise.reload expect(profile_enterprise.sells).to eq 'any' end + + + it "allows owner to be changed" do + controller.stub spree_current_user: admin_user + update_params = { id: distributor, enterprise: { owner_id: distributor_manager } } + spree_post :update, update_params + + distributor.reload + expect(distributor.owner).to eq distributor_manager + end + + it "allows managers to be changed" do + controller.stub spree_current_user: admin_user + update_params = { id: distributor, enterprise: { user_ids: [distributor_owner.id,distributor_manager.id,user.id] } } + spree_post :update, update_params + + distributor.reload + expect(distributor.users).to include user + end end end @@ -114,7 +145,7 @@ module Admin let(:enterprise) { create(:enterprise, sells: 'none') } before do - controller.stub spree_current_user: user + controller.stub spree_current_user: distributor_manager end context "as a normal user" do @@ -126,7 +157,7 @@ module Admin context "as a manager" do before do - enterprise.enterprise_roles.build(user: user).save + enterprise.enterprise_roles.build(user: distributor_manager).save end context "allows setting 'sells' to 'none'" do From 9fa5a9e766a23e03c5746dcf92af6b400620fe7a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 12 Feb 2015 10:58:32 +1100 Subject: [PATCH 639/681] Fix before_filter load order for enterprises#shop --- app/controllers/enterprises_controller.rb | 39 +++++++++++------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 77cebdb183..097139556c 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -2,6 +2,9 @@ class EnterprisesController < BaseController layout "darkswarm" helper Spree::ProductsHelper include OrderCyclesHelper + + # These prepended filters are in the reverse order of execution + prepend_before_filter :load_active_distributors, :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop before_filter :clean_permalink, only: :check_permalink respond_to :js, only: :permalink_checker @@ -56,26 +59,6 @@ class EnterprisesController < BaseController end end - def shop - distributor = Enterprise.is_distributor.find_by_permalink(params[:id]) || Enterprise.is_distributor.find(params[:id]) - order = current_order(true) - - if order.distributor and order.distributor != distributor - order.empty! - order.set_order_cycle! nil - end - - order.distributor = distributor - - order_cycle_options = OrderCycle.active.with_distributor(distributor) - order.order_cycle = order_cycle_options.first if order_cycle_options.count == 1 - order.save! - - require_distributor_chosen - set_order_cycles - load_active_distributors - end - def check_permalink return render text: params[:permalink], status: 409 if Enterprise.find_by_permalink params[:permalink] @@ -92,4 +75,20 @@ class EnterprisesController < BaseController def clean_permalink params[:permalink] = params[:permalink].parameterize end + + def reset_order + distributor = Enterprise.is_distributor.find_by_permalink(params[:id]) || Enterprise.is_distributor.find(params[:id]) + order = current_order(true) + + if order.distributor and order.distributor != distributor + order.empty! + order.set_order_cycle! nil + end + + order.distributor = distributor + + order_cycle_options = OrderCycle.active.with_distributor(distributor) + order.order_cycle = order_cycle_options.first if order_cycle_options.count == 1 + order.save! + end end From 5503760ce3d79f6d03c22b1f344c196738cae1b6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 12 Feb 2015 13:45:25 +1100 Subject: [PATCH 640/681] Scoping users correctly --- app/models/spree/user_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index c57e709cf9..1bafc85d0c 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -13,7 +13,7 @@ Spree.user_class.class_eval do def known_users if admin? - Spree::User + Spree::User.all else Spree::User .includes(:enterprises) From 12dc0b93aa9d23cf0eabc7c9990b4bd9581be04b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 12 Feb 2015 16:20:46 +1100 Subject: [PATCH 641/681] Scoping users correctly for realz this time --- app/models/spree/user_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 1bafc85d0c..d19de72d1b 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -13,7 +13,7 @@ Spree.user_class.class_eval do def known_users if admin? - Spree::User.all + Spree::User.scoped else Spree::User .includes(:enterprises) From 6b5a1255f896b77a7715c0a6f54c0146e523a446 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 12 Feb 2015 16:36:18 +1100 Subject: [PATCH 642/681] fixing a bunch of tests --- app/models/enterprise_group.rb | 23 +- .../admin/enterprise_groups/index.html.haml | 2 +- app/views/groups/show.html.haml | 223 ------------------ lib/spree/api/testing_support/setup.rb | 2 + spec/features/consumer/groups_spec.rb | 5 +- spec/models/enterprise_group_spec.rb | 11 - spec/models/model_set_spec.rb | 10 +- 7 files changed, 26 insertions(+), 250 deletions(-) diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 7012e208ed..60c9162a77 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -6,6 +6,7 @@ class EnterpriseGroup < ActiveRecord::Base belongs_to :address, :class_name => 'Spree::Address' accepts_nested_attributes_for :address validates :address, presence: true, associated: true + before_validation :set_undefined_address_fields before_validation :set_unused_address_fields validates :name, presence: true @@ -50,28 +51,38 @@ class EnterpriseGroup < ActiveRecord::Base address.firstname = address.lastname = 'unused' if address.present? end + def set_undefined_address_fields + if !address.present? + return + end + address.phone.present? || address.phone = 'undefined' + address.address1.present? || address.address1 = 'undefined' + address.city.present? || address.city = 'undefined' + address.zipcode.present? || address.zipcode = 'undefined' + end + def phone - address.phone.andand.sub('undefined', '') + address.andand.phone.andand.sub('undefined', '') end def address1 - address.address1.andand.sub('undefined', '') + address.andand.address1.andand.sub('undefined', '') end def address2 - address.address2.andand.sub('undefined', '') + address.andand.address2.andand.sub('undefined', '') end def city - address.city.andand.sub('undefined', '') + address.andand.city.andand.sub('undefined', '') end def state - address.state + address.andand.state end def zipcode - address.zipcode.andand.sub('undefined', '') + address.andand.zipcode.andand.sub('undefined', '') end end diff --git a/app/views/admin/enterprise_groups/index.html.haml b/app/views/admin/enterprise_groups/index.html.haml index 3ccf82c10c..13bc19e364 100644 --- a/app/views/admin/enterprise_groups/index.html.haml +++ b/app/views/admin/enterprise_groups/index.html.haml @@ -32,5 +32,5 @@ .blank-action - else = link_to_with_icon 'icon-arrow-down', '', main_app.admin_enterprise_group_move_down_path(enterprise_group), class: 'move-down no-text' - - if enterprise_group.first? + - if !enterprise_group.first? = link_to_with_icon 'icon-arrow-up', '', main_app.admin_enterprise_group_move_up_path(enterprise_group), class: 'move-up no-text' diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index fcf64a0892..98de889532 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -6,10 +6,6 @@ - if @group.promo_image.present? %img{"src" => @group.promo_image} .row - / .small-6.medium-2.large-1.columns.group-logo.pad-top - - / .small-6.medium-10.large-11.columns.group-header.pad-top - .small-12.columns.group-header.pad-top - if @group.logo.present? %img.group-logo{"src" => @group.logo} @@ -92,65 +88,6 @@ .small-12.medium-12.large-3.columns = render partial: 'contact' - / %h4 Contact us - / - if @group.phone - / .row - / .small-2.columns - / Call - / .small-10.columns - / = @group.phone - / - if @group.email - / .row - / .small-2.columns - / Email - / .small-10.columns - / = @group.email - / - if @group.website - / .row - / .small-2.columns - / Website - / .small-10.columns - / = @group.website - / %p   - / %h6 Address - / %p - / = @group.address.address1 - / - if @group.address.address2 - / %br - / = @group.address.address2 - / %br - / = @group.address.city - / , - / = @group.address.state - / = @group.address.zipcode - / %br - / = @group.address.country - / %p - / %h6 Follow us - / - if @group.facebook - / .row - / .small-2.columns - / Facebook - / .small-10.columns - / = @group.facebook - / - if @group.instagram - / .row - / .small-2.columns - / Instagram - / .small-10.columns - / = @group.instagram - / - if @group.linkedin - / .row - / .small-2.columns - / LinkedIn - / .small-10.columns - / = @group.linkedin - / - if @group.twitter - / .row - / .small-2.columns - / Twitter - / .small-10.columns - / = @group.twitter .small-12.columns.pad-top .row.pad-top @@ -168,164 +105,4 @@ %p   -// ORIGINAL MIKAEL STUFF - -/ %div{style: "padding: 1.5em;"} -/ %img{"src" => @group.promo_image, style: "display: block"} -/ %div{style: "margin-bottom: 1em"} -/ %img{"src" => @group.logo} -/ %div{style: "display: inline-block; vertical-align: middle"} -/ %h2{style: "margin-bottom: 0em"}= @group.name -/ %div= @group.description - -/ #div{"ng-controller" => "TabsCtrl", style: "clear: both"} -/ %tabset - -/ %tab{heading: 'Map', -/ active: "active(\'\')", -/ select: "select(\'\')"} -/ = inject_json_ams "enterprises", @group.enterprises, Api::EnterpriseSerializer -/ .map-container -/ %map{"ng-controller" => "MapCtrl", "ng-if" => "(active(\'\') && (mapShowed = true)) || mapShowed"} -/ %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} -/ %markers{models: "OfnMap.enterprises", fit: "true", -/ coords: "'self'", icon: "'icon'", click: "'reveal'"} - -/ %tab{heading: 'About us', -/ active: "active(\'about\')", -/ select: "select(\'about\')"} -/ %h3 About us -/ %p= @group.long_description - -/ %tab{heading: 'Our producers', -/ active: "active(\'producers\')", -/ select: "select(\'producers\')"} -/ .producers.pad-top{"ng-controller" => "GroupEnterprisesCtrl"} -/ .row -/ .small-12.columns.pad-top -/ %h1 Our Producers -/ = render partial: "shared/components/enterprise_search" -/ -# TODO: find out why this is not working -/ -#= render partial: "producers/filters" - -/ .row{bindonce: true} -/ .small-12.columns -/ .active_table -/ %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", -/ "ng-repeat" => "producer in filteredEnterprises = (Enterprises.producers | visible | searchEnterprises:query | taxons:activeTaxons)", -/ "ng-controller" => "GroupEnterpriseNodeCtrl", -/ "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !producer.active}", -/ id: "{{producer.hash}}"} - -/ .small-12.columns -/ = render partial: 'producers/skinny' -/ = render partial: 'producers/fat' - -/ = render partial: 'shared/components/enterprise_no_results' - -/ %tab{heading: 'Our hubs', -/ active: "active(\'hubs\')", -/ select: "select(\'hubs\')"} -/ #hubs.hubs{"ng-controller" => "GroupEnterprisesCtrl"} -/ .row -/ .small-12.columns -/ %h1 Our Hubs - -/ = render partial: "shared/components/enterprise_search" -/ -# TODO: find out why this is not working -/ -#= render partial: "home/filters" - -/ .row{bindonce: true} -/ .small-12.columns -/ .active_table -/ %hub.active_table_node.row.animate-repeat{id: "{{hub.hash}}", -/ "ng-repeat" => "hub in filteredEnterprises = (Enterprises.hubs | visible | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])", -/ "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", -/ "ng-controller" => "GroupEnterpriseNodeCtrl"} -/ .small-12.columns -/ = render partial: 'home/skinny' -/ = render partial: 'home/fat' - -/ = render partial: 'shared/components/enterprise_no_results' - -/ %tab{heading: 'Contact us', -/ active: "active(\'contact\')", -/ select: "select(\'contact\')"} -/ .row -/ .small-6.columns -/ %h3 Contact us -/ - if @group.phone -/ .row -/ .small-2.columns -/ Call -/ .small-10.columns -/ = @group.phone -/ - if @group.email -/ .row -/ .small-2.columns -/ Email -/ .small-10.columns -/ = @group.email -/ - if @group.website -/ .row -/ .small-2.columns -/ Website -/ .small-10.columns -/ = @group.website -/ .small-6.columns -/ %h3 Follow us -/ - if @group.facebook -/ .row -/ .small-2.columns -/ Facebook -/ .small-10.columns -/ = @group.facebook -/ - if @group.instagram -/ .row -/ .small-2.columns -/ Instagram -/ .small-10.columns -/ = @group.instagram -/ - if @group.linkedin -/ .row -/ .small-2.columns -/ LinkedIn -/ .small-10.columns -/ = @group.linkedin -/ - if @group.twitter -/ .row -/ .small-2.columns -/ Twitter -/ .small-10.columns -/ = @group.twitter -/ .row -/ .small-6.columns -/ %h3 Address -/ %p -/ = @group.address.address1 -/ - if @group.address.address2 -/ %br -/ = @group.address.address2 -/ %br -/ = @group.address.city -/ , -/ = @group.address.state -/ = @group.address.zipcode -/ %br -/ = @group.address.country - -/ .text-center -/ %p -/ = @group.name -/ %p -/ -if @group.facebook -/ %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/' + @group.facebook, target: '_blank'} -/ %i.ofn-i_044-facebook -/ -if @group.email -/ %a{title:'Email us', href: @group.email.reverse, mailto: true} -/ %i.ofn-i_050-mail-circle -/ -if @group.website -/ %a{title:'Visit our website', href: 'http://' + @group.website, target: '_blank'} -/ %i.ofn-i_049-web - = render partial: "shared/footer" diff --git a/lib/spree/api/testing_support/setup.rb b/lib/spree/api/testing_support/setup.rb index cbf8393e33..2b96b60946 100644 --- a/lib/spree/api/testing_support/setup.rb +++ b/lib/spree/api/testing_support/setup.rb @@ -7,6 +7,7 @@ module Spree user = stub_model(Spree::LegacyUser) user.stub(:has_spree_role?).with("admin").and_return(false) user.stub(:enterprises) { [] } + user.stub(:owned_groups) { [] } user end end @@ -33,6 +34,7 @@ module Spree # Stub enterprises, needed for cancan ability checks user.stub(:enterprises) { [] } + user.stub(:owned_groups) { [] } user end diff --git a/spec/features/consumer/groups_spec.rb b/spec/features/consumer/groups_spec.rb index b2db10d58c..e51ac39ea2 100644 --- a/spec/features/consumer/groups_spec.rb +++ b/spec/features/consumer/groups_spec.rb @@ -14,9 +14,6 @@ feature 'Groups', js: true do it "renders enterprise modals for groups" do visit groups_path - page.should have_content enterprise.name - open_enterprise_modal enterprise - modal_should_be_open_for enterprise - page.should have_content "Herndon, Vic" + page.should have_content group.name end end diff --git a/spec/models/enterprise_group_spec.rb b/spec/models/enterprise_group_spec.rb index 9642b146c7..ceca13b13b 100644 --- a/spec/models/enterprise_group_spec.rb +++ b/spec/models/enterprise_group_spec.rb @@ -61,15 +61,4 @@ describe EnterpriseGroup do EnterpriseGroup.managed_by(user).should == [eg1] end end - - describe "urls" do - it 'provides a Facebook URL' do - g = create(:enterprise_group) - g.facebook_url.should be_nil - g.facebook = 'test' - g.facebook_url.should == 'https://www.facebook.com/test' - g.facebook = 'http://fb.com/test' - g.facebook_url.should == g.facebook - end - end end diff --git a/spec/models/model_set_spec.rb b/spec/models/model_set_spec.rb index 24f452412f..e45018747c 100644 --- a/spec/models/model_set_spec.rb +++ b/spec/models/model_set_spec.rb @@ -3,14 +3,14 @@ require 'spec_helper' describe ModelSet do describe "updating" do it "creates new models" do - attrs = {collection_attributes: {'1' => {name: 'e1', description: 'foo'}, - '2' => {name: 'e2', description: 'bar'}}} + attrs = {collection_attributes: {'1' => {name: 's1'}, + '2' => {name: 's2'}}} - ms = ModelSet.new(EnterpriseGroup, EnterpriseGroup.all, attrs) + ms = ModelSet.new(Suburb, Suburb.all, attrs) - expect { ms.save }.to change(EnterpriseGroup, :count).by(2) + expect { ms.save }.to change(Suburb, :count).by(2) - EnterpriseGroup.where(name: ['e1', 'e2']).count.should == 2 + Suburb.where(name: ['s1', 's2']).count.should == 2 end From 1d61e91afdb3eb4bf7d0869cd53347d3b6cce4f5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 11 Feb 2015 15:41:06 +1100 Subject: [PATCH 643/681] When enterprise created, grant permission to all co-owned enterprises, not just hubs --- app/models/enterprise.rb | 13 ++++++++----- spec/models/enterprise_spec.rb | 18 +++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 81615afd2d..97f98b288e 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -68,7 +68,7 @@ class Enterprise < ActiveRecord::Base before_validation :set_unused_address_fields after_validation :geocode_address - after_create :relate_to_owners_hubs + after_create :relate_to_owners_enterprises # TODO: Later versions of devise have a dedicated after_confirmation callback, so use that after_update :welcome_after_confirm, if: lambda { confirmation_token_changed? && confirmation_token.nil? } after_create :send_welcome_email, if: lambda { email_is_known? } @@ -365,12 +365,15 @@ class Enterprise < ActiveRecord::Base end end - def relate_to_owners_hubs - hubs = owner.owned_enterprises.is_hub.where('enterprises.id != ?', self) + def relate_to_owners_enterprises + # When a new enterprise is created, we relate them to all enterprises owned by + # the same owner. - hubs.each do |hub| + enterprises = owner.owned_enterprises.where('enterprises.id != ?', self) + + enterprises.each do |enterprise| EnterpriseRelationship.create!(parent: self, - child: hub, + child: enterprise, permissions_list: [:add_to_order_cycle, :manage_products, :edit_profile, diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 15bc7c2bbd..1b285eecca 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -601,27 +601,31 @@ describe Enterprise do let(:er1) { EnterpriseRelationship.where(child_id: hub1).last } let(:er2) { EnterpriseRelationship.where(child_id: hub2).last } + let(:er3) { EnterpriseRelationship.where(child_id: producer).last } - it "establishes relationships with the owner's hubs" do + it "establishes relationships for new hubs with the owner's hubs and producers" do hub1 hub2 + producer enterprise = nil expect do enterprise = create(:enterprise, owner: owner) - end.to change(EnterpriseRelationship, :count).by(2) + end.to change(EnterpriseRelationship, :count).by(3) - [er1, er2].each do |er| + [er1, er2, er3].each do |er| er.parent.should == enterprise er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort end end - it "doesn't relate to enterprises that aren't sells=='any'" do - producer + it "establishes relationships when producers are created" do + hub1 + hub2 + expect do - enterprise = create(:enterprise, owner: owner) - end.to change(EnterpriseRelationship, :count).by(0) + producer + end.to change(EnterpriseRelationship, :count).by(2) end end end From 636ed2ad0d9861826aad6c9fe1875ff4bc30bc07 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 12 Feb 2015 09:59:55 +1100 Subject: [PATCH 644/681] Make bi-directional relationships --- app/models/enterprise.rb | 10 +++++++++- spec/models/enterprise_spec.rb | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 97f98b288e..8f5c940e77 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -367,7 +367,8 @@ class Enterprise < ActiveRecord::Base def relate_to_owners_enterprises # When a new enterprise is created, we relate them to all enterprises owned by - # the same owner. + # the same owner, in both directions. So all enterprises owned by the same owner + # will have permissions to every other one, in both directions. enterprises = owner.owned_enterprises.where('enterprises.id != ?', self) @@ -378,6 +379,13 @@ class Enterprise < ActiveRecord::Base :manage_products, :edit_profile, :create_variant_overrides]) + + EnterpriseRelationship.create!(parent: enterprise, + child: self, + permissions_list: [:add_to_order_cycle, + :manage_products, + :edit_profile, + :create_variant_overrides]) end end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 1b285eecca..494a543260 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -602,8 +602,11 @@ describe Enterprise do let(:er1) { EnterpriseRelationship.where(child_id: hub1).last } let(:er2) { EnterpriseRelationship.where(child_id: hub2).last } let(:er3) { EnterpriseRelationship.where(child_id: producer).last } + let(:er4) { EnterpriseRelationship.where(parent_id: hub1).last } + let(:er5) { EnterpriseRelationship.where(parent_id: hub2).last } + let(:er6) { EnterpriseRelationship.where(parent_id: producer).last } - it "establishes relationships for new hubs with the owner's hubs and producers" do + it "establishes bi-directional relationships for new hubs with the owner's hubs and producers" do hub1 hub2 producer @@ -611,21 +614,26 @@ describe Enterprise do expect do enterprise = create(:enterprise, owner: owner) - end.to change(EnterpriseRelationship, :count).by(3) + end.to change(EnterpriseRelationship, :count).by(6) [er1, er2, er3].each do |er| er.parent.should == enterprise er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort end + + [er4, er5, er6].each do |er| + er.child.should == enterprise + er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort + end end - it "establishes relationships when producers are created" do + it "establishes bi-directional relationships when producers are created" do hub1 hub2 expect do producer - end.to change(EnterpriseRelationship, :count).by(2) + end.to change(EnterpriseRelationship, :count).by(4) end end end From 32a898b6a2f90d263993f828f1b4760c9e1335cc Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 13 Feb 2015 09:26:34 +1100 Subject: [PATCH 645/681] Remove unnecessary db:schema:load db:seed, these are performed by db:setup --- README.markdown | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.markdown b/README.markdown index 8c9883dc04..af194a0083 100644 --- a/README.markdown +++ b/README.markdown @@ -53,14 +53,10 @@ Configure the site: cp config/application.yml.example config/application.yml edit config/application.yml -Create the development and test databases, using the settings specified in `config/database.yml`: +Create the development and test databases, using the settings specified in `config/database.yml`, and populate them with a schema and seed data: rake db:setup -Then load the schema and some seed data with the following command: - - rake db:schema:load db:seed - Load some default data for your environment: rake openfoodnetwork:dev:load_sample_data From 87686848bccd1745f659fb358bc758c9e4b10599 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 13 Feb 2015 15:39:44 +1100 Subject: [PATCH 646/681] make group contact fields not null --- ...0121030627_add_web_conact_to_enterprise_groups.rb | 10 ---------- ...121030627_add_web_contact_to_enterprise_groups.rb | 10 ++++++++++ db/schema.rb | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) delete mode 100644 db/migrate/20150121030627_add_web_conact_to_enterprise_groups.rb create mode 100644 db/migrate/20150121030627_add_web_contact_to_enterprise_groups.rb diff --git a/db/migrate/20150121030627_add_web_conact_to_enterprise_groups.rb b/db/migrate/20150121030627_add_web_conact_to_enterprise_groups.rb deleted file mode 100644 index 1aca6a0e16..0000000000 --- a/db/migrate/20150121030627_add_web_conact_to_enterprise_groups.rb +++ /dev/null @@ -1,10 +0,0 @@ -class AddWebConactToEnterpriseGroups < ActiveRecord::Migration - def change - add_column :enterprise_groups, :email, :string - add_column :enterprise_groups, :website, :string - add_column :enterprise_groups, :facebook, :string - add_column :enterprise_groups, :instagram, :string - add_column :enterprise_groups, :linkedin, :string - add_column :enterprise_groups, :twitter, :string - end -end diff --git a/db/migrate/20150121030627_add_web_contact_to_enterprise_groups.rb b/db/migrate/20150121030627_add_web_contact_to_enterprise_groups.rb new file mode 100644 index 0000000000..afea2dbeea --- /dev/null +++ b/db/migrate/20150121030627_add_web_contact_to_enterprise_groups.rb @@ -0,0 +1,10 @@ +class AddWebContactToEnterpriseGroups < ActiveRecord::Migration + def change + add_column :enterprise_groups, :email, :string, null: false, default: '' + add_column :enterprise_groups, :website, :string, null: false, default: '' + add_column :enterprise_groups, :facebook, :string, null: false, default: '' + add_column :enterprise_groups, :instagram, :string, null: false, default: '' + add_column :enterprise_groups, :linkedin, :string, null: false, default: '' + add_column :enterprise_groups, :twitter, :string, null: false, default: '' + end +end diff --git a/db/schema.rb b/db/schema.rb index c92989be2f..fe9d3f8ca2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -198,12 +198,12 @@ ActiveRecord::Schema.define(:version => 20150202000203) do t.integer "logo_file_size" t.datetime "logo_updated_at" t.integer "address_id" - t.string "email" - t.string "website" - t.string "facebook" - t.string "instagram" - t.string "linkedin" - t.string "twitter" + t.string "email", :default => "", :null => false + t.string "website", :default => "", :null => false + t.string "facebook", :default => "", :null => false + t.string "instagram", :default => "", :null => false + t.string "linkedin", :default => "", :null => false + t.string "twitter", :default => "", :null => false t.integer "owner_id" end From b9248d3e2c3084fb4bc51cdf09d50b2812b5aaa4 Mon Sep 17 00:00:00 2001 From: Liv Galendez Date: Wed, 18 Feb 2015 10:34:23 +1100 Subject: [PATCH 647/681] Fixed typo in empty cart error message --- .../javascripts/darkswarm/directives/empties_cart.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee b/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee index b49131afa6..51e960867b 100644 --- a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee @@ -8,6 +8,6 @@ Darkswarm.directive "ofnEmptiesCart", (CurrentHub, Cart, Navigation, storage) -> if CurrentHub.hub?.id and CurrentHub.hub.id isnt scope.hub.id and !Cart.empty() elm.bind 'click', (ev)-> ev.preventDefault() - if confirm "Are you sure? This will change your selected Hub and remove any items in you shopping cart." + if confirm "Are you sure? This will change your selected Hub and remove any items in your shopping cart." storage.clearAll() # One day this will have to be moar GRANULAR Navigation.go scope.hub.path From 9496987da6789bd203412b6fedf173d3ddba7dc3 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 18 Feb 2015 12:04:17 +1100 Subject: [PATCH 648/681] Fix display of admin group side menu --- .../controllers/side_menu_controller.js.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee index 63f05a9376..b30b160bd0 100644 --- a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee @@ -12,3 +12,6 @@ angular.module("admin.enterprise_groups") ] $scope.select(0) + + $scope.showItem = (item) -> + true From 08afcac1e58f036678ff621e04a118f4a7637ec5 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 18 Feb 2015 14:55:42 +1100 Subject: [PATCH 649/681] exposing more group attributes to angular --- app/views/groups/index.html.haml | 3 +++ app/views/json/_groups.rabl | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 1688446a7f..24b0389a89 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -28,6 +28,9 @@ %p %em {{ group.description }} + /{{ group.state }} + /{{ group.email }} + /{{ group.twitter }} / .small-12.medium-3.columns / {{ group.address.state }} / .small-6.columns.text-right diff --git a/app/views/json/_groups.rabl b/app/views/json/_groups.rabl index 691d36f42f..a079290e23 100644 --- a/app/views/json/_groups.rabl +++ b/app/views/json/_groups.rabl @@ -1,5 +1,5 @@ collection @groups -attributes :id, :name, :position, :description, :long_description +attributes :id, :name, :position, :description, :long_description, :email, :website, :facebook, :instagram, :linkedin, :twitter child enterprises: :enterprises do attributes :id @@ -12,3 +12,7 @@ end node :promo_image do |group| group.promo_image(:large) if group.promo_image.exists? end + +node :state do |group| + group.state().andand.abbr +end From f83ceae5d136a25240fdcbe2de6f69acb33c20c3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 18 Feb 2015 15:32:23 +1100 Subject: [PATCH 650/681] Fix feature: Enterprises list is shortened when error in bulk update --- app/controllers/admin/enterprises_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 503fd736ba..aa8276020d 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -49,6 +49,8 @@ module Admin flash[:success] = 'Enterprises updated successfully' redirect_to main_app.admin_enterprises_path else + touched_ids = params[:enterprise_set][:collection_attributes].values.map { |v| v[:id].to_i } + @enterprise_set.collection.select! { |e| touched_ids.include? e.id } flash[:error] = 'Update failed' render :index end From 346a4e71d048a4de9c263dc9f4e87f26d14ec510 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 10:17:58 +1100 Subject: [PATCH 651/681] side menu partial does not require showItem defined --- .../controllers/side_menu_controller.js.coffee | 3 --- app/views/admin/shared/_side_menu.html.haml | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee index b30b160bd0..63f05a9376 100644 --- a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee @@ -12,6 +12,3 @@ angular.module("admin.enterprise_groups") ] $scope.select(0) - - $scope.showItem = (item) -> - true diff --git a/app/views/admin/shared/_side_menu.html.haml b/app/views/admin/shared/_side_menu.html.haml index ff5d086570..12d1013825 100644 --- a/app/views/admin/shared/_side_menu.html.haml +++ b/app/views/admin/shared/_side_menu.html.haml @@ -2,7 +2,7 @@ %a.menu_item{ href: "", id: "{{ item.name.toLowerCase().replace(' ', '_') }}", ng: { repeat: '(index,item) in menu.items | filter:{visible:true}', click: 'select(index)', - show: 'showItem(item)', + show: 'showItem(item) !== false', class: '{ selected: item.selected}', 'class-odd' => "'odd'", 'class-even' => "'even'" } } From bbac5aa8037015d75ce9cca82f55c8f247a5a03b Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 10:22:37 +1100 Subject: [PATCH 652/681] Using pure css for styling odd and even menu items --- app/assets/stylesheets/admin/side_menu.css.sass | 4 ++-- app/views/admin/shared/_side_menu.html.haml | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/admin/side_menu.css.sass b/app/assets/stylesheets/admin/side_menu.css.sass index 6c03059d27..c218cfbcf0 100644 --- a/app/assets/stylesheets/admin/side_menu.css.sass +++ b/app/assets/stylesheets/admin/side_menu.css.sass @@ -7,9 +7,9 @@ font-size: 120% cursor: pointer text-transform: uppercase - &.odd + &:nth-child(odd) background-color: #ebf3fb - &.even + &:nth-child(even) background-color: #ffffff &:hover background-color: #eaf0f5 diff --git a/app/views/admin/shared/_side_menu.html.haml b/app/views/admin/shared/_side_menu.html.haml index 12d1013825..44efb2475c 100644 --- a/app/views/admin/shared/_side_menu.html.haml +++ b/app/views/admin/shared/_side_menu.html.haml @@ -3,8 +3,6 @@ ng: { repeat: '(index,item) in menu.items | filter:{visible:true}', click: 'select(index)', show: 'showItem(item) !== false', - class: '{ selected: item.selected}', - 'class-odd' => "'odd'", - 'class-even' => "'even'" } } + class: '{ selected: item.selected }' } } %i{ class: "{{item.icon_class}}" } %span {{ item.name }} From 718a5911a3ea7086b3407251962bbfa1febc34b2 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 10:48:39 +1100 Subject: [PATCH 653/681] code style and cleanup --- .../group_enterprise_node_controller.js.coffee | 2 +- .../darkswarm/controllers/tabs_controller.js.coffee | 2 +- app/helpers/groups_helper.rb | 8 ++++---- app/models/enterprise_group.rb | 6 ++---- app/views/admin/enterprise_groups/_form.html.haml | 6 +++--- ...add_address_instances_to_existing_enterprise_groups.rb | 4 +--- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee index 31c2354a53..376320e458 100644 --- a/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee @@ -2,7 +2,7 @@ Darkswarm.controller "GroupEnterpriseNodeCtrl", ($scope, CurrentHub) -> $scope.active = false - $scope.toggle = () -> + $scope.toggle = -> $scope.active = !$scope.active $scope.open = -> diff --git a/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee index 74ce167580..fad13164ff 100644 --- a/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee @@ -4,7 +4,7 @@ Darkswarm.controller "TabsCtrl", ($scope, $rootScope, $location) -> $location.hash() == path # Select tab by setting the url hash path. - $scope.select= (path)-> + $scope.select = (path)-> $location.hash path # Toggle tab selected status by setting the url hash path. diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 6ca71a7b78..c5de5c1ca0 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,7 +1,7 @@ module GroupsHelper def link_to_service(baseurl, name, html_options = {}) - if name.blank? then return end + return if name.blank? html_options = html_options.merge target: '_blank' link_to ext_url(baseurl, name), html_options do yield @@ -9,10 +9,10 @@ module GroupsHelper end def ext_url(prefix, url) - if (url =~ /^https?:\/\//i) - return url + if url =~ /^https?:\/\//i + url else - return prefix + url + prefix + url end end diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 60c9162a77..582518e208 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -43,7 +43,7 @@ class EnterpriseGroup < ActiveRecord::Base if user.has_spree_role?('admin') scoped else - where('owner_id = ?', user.id); + where('owner_id = ?', user.id) end } @@ -52,9 +52,7 @@ class EnterpriseGroup < ActiveRecord::Base end def set_undefined_address_fields - if !address.present? - return - end + return unless address.present? address.phone.present? || address.phone = 'undefined' address.address1.present? || address.address1 = 'undefined' address.city.present? || address.city = 'undefined' diff --git a/app/views/admin/enterprise_groups/_form.html.haml b/app/views/admin/enterprise_groups/_form.html.haml index 9ebf6db147..c72c566e46 100644 --- a/app/views/admin/enterprise_groups/_form.html.haml +++ b/app/views/admin/enterprise_groups/_form.html.haml @@ -1,4 +1,4 @@ -= render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise } += render 'spree/shared/error_messages', target: @enterprise = form_for [main_app, :admin, @enterprise_group] do |f| .row{ ng: {app: 'admin.enterprise_groups', controller: 'enterpriseGroupCtrl'} } @@ -7,5 +7,5 @@ = render 'admin/shared/side_menu' .one.column   .eleven.columns.omega.fullwidth_inputs - = render :partial => 'inputs', :locals => { :f => f } - = render partial: "spree/admin/shared/#{action}_resource_links" + = render 'inputs', f: f + = render "spree/admin/shared/#{action}_resource_links" diff --git a/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb b/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb index 2978d286dc..0800948126 100644 --- a/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb +++ b/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb @@ -3,9 +3,7 @@ class AddAddressInstancesToExistingEnterpriseGroups < ActiveRecord::Migration country = Spree::Country.find_by_name(ENV['DEFAULT_COUNTRY']) state = country.states.first EnterpriseGroup.all.each do |g| - if g.address.present? then - next - end + next if g.address.present? address = Spree::Address.new(firstname: 'unused', lastname: 'unused', address1: 'undefined', city: 'undefined', zipcode: 'undefined', state: state, country: country, phone: 'undefined') g.address = address g.save From ecd7b16ef515584127a5867c58e9a13a82cff4c6 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 11:09:43 +1100 Subject: [PATCH 654/681] revert setting default country by name in enterprise controller --- app/controllers/admin/enterprises_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index ac852c0886..503fd736ba 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -59,7 +59,7 @@ module Admin def build_resource_with_address enterprise = build_resource_without_address enterprise.address = Spree::Address.new - enterprise.address.country = Spree::Country.find_by_name(ENV['DEFAULT_COUNTRY']) + enterprise.address.country = Spree::Country.find_by_id(Spree::Config[:default_country_id]) enterprise end alias_method_chain :build_resource, :address From a0f0b3c93b2cc84dea36e28178a39dcacf3c22dd Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 11:14:25 +1100 Subject: [PATCH 655/681] display only activated enterprises to add to groups --- app/views/admin/enterprise_groups/_inputs.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/enterprise_groups/_inputs.html.haml b/app/views/admin/enterprise_groups/_inputs.html.haml index 60181e09c4..a9e911700a 100644 --- a/app/views/admin/enterprise_groups/_inputs.html.haml +++ b/app/views/admin/enterprise_groups/_inputs.html.haml @@ -28,7 +28,7 @@ = f.field_container :enterprise_ids do = f.label :enterprise_ids, 'Enterprises' %br/ - = f.collection_select :enterprise_ids, Enterprise.all, :id, :name, {}, {class: "select2 fullwidth", multiple: true} + = f.collection_select :enterprise_ids, Enterprise.activated, :id, :name, {}, {class: "select2 fullwidth", multiple: true} %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } %legend About From 8e83c6679a47ff7fcf2dcb857f6702437438ae15 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 11:25:31 +1100 Subject: [PATCH 656/681] extended test of owned groups --- spec/models/spree/user_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index a1a9f3c940..8510b89e0e 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -30,9 +30,11 @@ describe Spree.user_class do let(:u2) { create(:user) } let!(:g1) { create(:enterprise_group, owner: u1) } let!(:g2) { create(:enterprise_group, owner: u1) } + let!(:g3) { create(:enterprise_group, owner: u2) } it "provides access to owned groups" do - expect(u1.owned_groups(:reload)).to include g1, g2 + expect(u1.owned_groups(:reload)).to eq [g1, g2] + expect(u2.owned_groups(:reload)).to eq [g3] end end From 6a29b830c261c6dba8ecf604f95f174831aefcbc Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 11:35:06 +1100 Subject: [PATCH 657/681] commenting changes in overridden adaptivemenu.js --- vendor/assets/javascripts/jquery.adaptivemenu.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vendor/assets/javascripts/jquery.adaptivemenu.js b/vendor/assets/javascripts/jquery.adaptivemenu.js index 8df8b98c65..b9a611a8fb 100644 --- a/vendor/assets/javascripts/jquery.adaptivemenu.js +++ b/vendor/assets/javascripts/jquery.adaptivemenu.js @@ -10,7 +10,7 @@ jQuery.fn.AdaptiveMenu = function(options){ var options = jQuery.extend({ text: "More...", - accuracy:0, + accuracy:0, // originally 70, but not needed anymore 'class':null, 'classLinckMore':null },options); @@ -26,6 +26,7 @@ jQuery.fn.AdaptiveMenu = function(options){ }); var buildingMenu = function(){ + // Using parent width instead of given window width var windowWidth = $(menu.parent()).width() - options.accuracy; for(var i = 0; i windowWidth ) @@ -55,6 +56,7 @@ jQuery.fn.AdaptiveMenu = function(options){ } } + // calling buildingMenu without parameter jQuery(window).width() jQuery(window).resize(buildingMenu); jQuery(window).ready(buildingMenu); From a7b723af93684ee9e594a6f7b5e74d8873c633fd Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 13:23:50 +1100 Subject: [PATCH 658/681] owner index for enterprise groups --- .../20150115050935_add_addresses_ref_to_enterprise_groups.rb | 2 +- db/migrate/20150202000203_add_owner_to_enterprise_groups.rb | 2 +- .../20150219021742_add_owner_index_to_enterprise_groups.rb | 5 +++++ db/schema.rb | 4 +++- 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20150219021742_add_owner_index_to_enterprise_groups.rb diff --git a/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb b/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb index 8b2ca415b1..566b35bb88 100644 --- a/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb +++ b/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb @@ -1,6 +1,6 @@ class AddAddressesRefToEnterpriseGroups < ActiveRecord::Migration def change add_column :enterprise_groups, :address_id, :integer - add_foreign_key :enterprise_groups, :spree_addresses, name: "enterprise_groups_address_id_fk", column: "address_id" + add_foreign_key :enterprise_groups, :spree_addresses, column: "address_id" end end diff --git a/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb b/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb index 82ffdf9f5d..f2e46452df 100644 --- a/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb +++ b/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb @@ -1,6 +1,6 @@ class AddOwnerToEnterpriseGroups < ActiveRecord::Migration def change add_column :enterprise_groups, :owner_id, :integer - add_foreign_key :enterprise_groups, :spree_users, name: "enterprise_groups_owner_id_fk", column: "owner_id" + add_foreign_key :enterprise_groups, :spree_users, column: "owner_id" end end diff --git a/db/migrate/20150219021742_add_owner_index_to_enterprise_groups.rb b/db/migrate/20150219021742_add_owner_index_to_enterprise_groups.rb new file mode 100644 index 0000000000..062e9523e6 --- /dev/null +++ b/db/migrate/20150219021742_add_owner_index_to_enterprise_groups.rb @@ -0,0 +1,5 @@ +class AddOwnerIndexToEnterpriseGroups < ActiveRecord::Migration + def change + add_index :enterprise_groups, :owner_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 5c567fb57a..959e0f06d6 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 => 20150202000203) do +ActiveRecord::Schema.define(:version => 20150219021742) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -207,6 +207,8 @@ ActiveRecord::Schema.define(:version => 20150202000203) do t.integer "owner_id" end + add_index "enterprise_groups", ["owner_id"], :name => "index_enterprise_groups_on_owner_id" + create_table "enterprise_groups_enterprises", :id => false, :force => true do |t| t.integer "enterprise_group_id" t.integer "enterprise_id" From d5c2abdd7ea8e6f0eb7c2a648ced2eef0edd7077 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 16:33:43 +1100 Subject: [PATCH 659/681] stripping "undefined" in after_find and after_save callbacks instead of overriding getters --- app/models/enterprise_group.rb | 32 ++++++------------- .../admin/enterprise_groups/_inputs.html.haml | 4 --- app/views/groups/_contact.html.haml | 9 +++--- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 582518e208..aaf33b2272 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -8,6 +8,8 @@ class EnterpriseGroup < ActiveRecord::Base validates :address, presence: true, associated: true before_validation :set_undefined_address_fields before_validation :set_unused_address_fields + after_find :unset_undefined_address_fields + after_save :unset_undefined_address_fields validates :name, presence: true validates :description, presence: true @@ -18,7 +20,7 @@ class EnterpriseGroup < ActiveRecord::Base attr_accessible :address_attributes attr_accessible :email, :website, :facebook, :instagram, :linkedin, :twitter - delegate :phone, :to => :address + delegate :phone, :address1, :address2, :city, :zipcode, :state, :country, :to => :address has_attached_file :logo, styles: {medium: "100x100"}, @@ -59,28 +61,12 @@ class EnterpriseGroup < ActiveRecord::Base address.zipcode.present? || address.zipcode = 'undefined' end - def phone - address.andand.phone.andand.sub('undefined', '') - end - - def address1 - address.andand.address1.andand.sub('undefined', '') - end - - def address2 - address.andand.address2.andand.sub('undefined', '') - end - - def city - address.andand.city.andand.sub('undefined', '') - end - - def state - address.andand.state - end - - def zipcode - address.andand.zipcode.andand.sub('undefined', '') + def unset_undefined_address_fields + return unless address.present? + address.phone.sub!(/^undefined$/, '') + address.address1.sub!(/^undefined$/, '') + address.city.sub!(/^undefined$/, '') + address.zipcode.sub!(/^undefined$/, '') end end diff --git a/app/views/admin/enterprise_groups/_inputs.html.haml b/app/views/admin/enterprise_groups/_inputs.html.haml index a9e911700a..02f5aa337c 100644 --- a/app/views/admin/enterprise_groups/_inputs.html.haml +++ b/app/views/admin/enterprise_groups/_inputs.html.haml @@ -63,13 +63,11 @@ .row .alpha.three.columns = af.label :phone - %span.required * .omega.eight.columns = af.text_field :phone, { placeholder: "eg. 98 7654 3210"} .row .three.columns.alpha = af.label :address1 - %span.required * .eight.columns.omega = af.text_field :address1, { placeholder: "eg. 123 High Street"} .row @@ -82,7 +80,6 @@ = af.label :city, 'Suburb' \/ = af.label :zipcode, 'Postcode' - %span.required * .four.columns = af.text_field :city, { placeholder: "eg. Northcote"} .four.columns.omega @@ -92,7 +89,6 @@ = af.label :state_id, 'State' \/ = af.label :country_id, 'Country' - %span.required * .four.columns = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" .four.columns.omega diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index 458aa81dbe..c3da5e26ba 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -42,9 +42,10 @@ - if @group.address2.present? %br = @group.address2 - %br - = @group.city - = @group.state - = @group.zipcode + - if @group.city.present? + %br + = @group.city + = @group.state + = @group.zipcode From c01d45e3af9c7d7894647283894f91229cedfef6 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 16:51:58 +1100 Subject: [PATCH 660/681] Serialize move_up and move_down methods of groups There have been race conditions in other projects using the acts_as_list gem which could be solved by serializing. --- .../admin/enterprise_groups_controller.rb | 12 ++++++++---- app/models/enterprise_group.rb | 2 ++ lib/open_food_network/locking.rb | 13 +++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 lib/open_food_network/locking.rb diff --git a/app/controllers/admin/enterprise_groups_controller.rb b/app/controllers/admin/enterprise_groups_controller.rb index 3968cc5289..7759f57c12 100644 --- a/app/controllers/admin/enterprise_groups_controller.rb +++ b/app/controllers/admin/enterprise_groups_controller.rb @@ -7,14 +7,18 @@ module Admin end def move_up - @enterprise_group = EnterpriseGroup.find params[:enterprise_group_id] - @enterprise_group.move_higher + EnterpriseGroup.with_isolation_level_serializable do + @enterprise_group = EnterpriseGroup.find params[:enterprise_group_id] + @enterprise_group.move_higher + end redirect_to main_app.admin_enterprise_groups_path end def move_down - @enterprise_group = EnterpriseGroup.find params[:enterprise_group_id] - @enterprise_group.move_lower + EnterpriseGroup.with_isolation_level_serializable do + @enterprise_group = EnterpriseGroup.find params[:enterprise_group_id] + @enterprise_group.move_lower + end redirect_to main_app.admin_enterprise_groups_path end diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index aaf33b2272..9d510d8f2b 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -1,3 +1,5 @@ +require 'open_food_network/locking' + class EnterpriseGroup < ActiveRecord::Base acts_as_list diff --git a/lib/open_food_network/locking.rb b/lib/open_food_network/locking.rb new file mode 100644 index 0000000000..8c24256bf3 --- /dev/null +++ b/lib/open_food_network/locking.rb @@ -0,0 +1,13 @@ +module OpenFoodNetwork::Locking + # http://rhnh.net/2010/06/30/acts-as-list-will-break-in-production + def with_isolation_level_serializable + self.transaction do + self.connection.execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE") + yield + end + end +end + +class ActiveRecord::Base + extend OpenFoodNetwork::Locking +end From 98063dae60e56aeb68b8fededaf4680c91a7da30 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 19 Feb 2015 16:57:19 +1100 Subject: [PATCH 661/681] setting default country by id in groups controller --- app/controllers/admin/enterprise_groups_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/enterprise_groups_controller.rb b/app/controllers/admin/enterprise_groups_controller.rb index 7759f57c12..32280b7fa3 100644 --- a/app/controllers/admin/enterprise_groups_controller.rb +++ b/app/controllers/admin/enterprise_groups_controller.rb @@ -27,7 +27,7 @@ module Admin def build_resource_with_address enterprise_group = build_resource_without_address enterprise_group.address = Spree::Address.new - enterprise_group.address.country = Spree::Country.find_by_name(ENV['DEFAULT_COUNTRY']) + enterprise_group.address.country = Spree::Country.find_by_id(Spree::Config[:default_country_id]) enterprise_group end alias_method_chain :build_resource, :address From 176db78e6470680dc1fb633481305ceee0ae7ed5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 20 Feb 2015 11:17:01 +1100 Subject: [PATCH 662/681] Extract data fetching from views --- app/controllers/admin/enterprise_groups_controller.rb | 11 +++++++++-- app/views/admin/enterprise_groups/_inputs.html.haml | 5 ++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/controllers/admin/enterprise_groups_controller.rb b/app/controllers/admin/enterprise_groups_controller.rb index 32280b7fa3..3f8888edfb 100644 --- a/app/controllers/admin/enterprise_groups_controller.rb +++ b/app/controllers/admin/enterprise_groups_controller.rb @@ -1,6 +1,7 @@ module Admin class EnterpriseGroupsController < ResourceController - before_filter :load_countries, :except => :index + before_filter :load_data, except: :index + before_filter :load_object_data, only: [:new, :edit, :create, :update] def index @enterprise_groups = @enterprise_groups.managed_by(spree_current_user) @@ -34,10 +35,16 @@ module Admin private - def load_countries + def load_data @countries = Spree::Country.order(:name) + @enterprises = Enterprise.activated end + def load_object_data + @owner_email = @enterprise_group.andand.owner.andand.email || "" + end + + def collection EnterpriseGroup.by_position end diff --git a/app/views/admin/enterprise_groups/_inputs.html.haml b/app/views/admin/enterprise_groups/_inputs.html.haml index 02f5aa337c..dbf8606af6 100644 --- a/app/views/admin/enterprise_groups/_inputs.html.haml +++ b/app/views/admin/enterprise_groups/_inputs.html.haml @@ -17,8 +17,7 @@ .with-tip{'data-powertip' => "The primary user responsible for this group."} %a What's this? .eight.columns.omega - - owner_email = @enterprise_group.andand.owner.andand.email || "" - = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: owner_email + = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: @owner_email = f.field_container :on_front_page do = f.label :on_front_page, 'On front page?' @@ -28,7 +27,7 @@ = f.field_container :enterprise_ids do = f.label :enterprise_ids, 'Enterprises' %br/ - = f.collection_select :enterprise_ids, Enterprise.activated, :id, :name, {}, {class: "select2 fullwidth", multiple: true} + = f.collection_select :enterprise_ids, @enterprises, :id, :name, {}, {class: "select2 fullwidth", multiple: true} %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } %legend About From 4554c0555d33e8cc99fc036b9a692e33d74120ee Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 20 Feb 2015 11:31:39 +1100 Subject: [PATCH 663/681] Change locking to use postgres syntax, fixes 'must be first statement in transaction' error --- lib/open_food_network/locking.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/open_food_network/locking.rb b/lib/open_food_network/locking.rb index 8c24256bf3..30dec63b0b 100644 --- a/lib/open_food_network/locking.rb +++ b/lib/open_food_network/locking.rb @@ -2,7 +2,7 @@ module OpenFoodNetwork::Locking # http://rhnh.net/2010/06/30/acts-as-list-will-break-in-production def with_isolation_level_serializable self.transaction do - self.connection.execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE") + self.connection.execute "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE" yield end end From f58219eb3d39423431c97db465beb32e42b05cbd Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 20 Feb 2015 11:36:30 +1100 Subject: [PATCH 664/681] Split inputs into separate partials for each fieldset --- .../admin/enterprise_groups/_form.html.haml | 6 +- .../enterprise_groups/_form_about.html.haml | 6 + .../enterprise_groups/_form_address.html.haml | 36 +++++ .../enterprise_groups/_form_images.html.haml | 18 +++ .../_form_primary_details.html.haml | 30 +++++ .../enterprise_groups/_form_web.html.haml | 32 +++++ .../admin/enterprise_groups/_inputs.html.haml | 127 ------------------ 7 files changed, 127 insertions(+), 128 deletions(-) create mode 100644 app/views/admin/enterprise_groups/_form_about.html.haml create mode 100644 app/views/admin/enterprise_groups/_form_address.html.haml create mode 100644 app/views/admin/enterprise_groups/_form_images.html.haml create mode 100644 app/views/admin/enterprise_groups/_form_primary_details.html.haml create mode 100644 app/views/admin/enterprise_groups/_form_web.html.haml delete mode 100644 app/views/admin/enterprise_groups/_inputs.html.haml diff --git a/app/views/admin/enterprise_groups/_form.html.haml b/app/views/admin/enterprise_groups/_form.html.haml index c72c566e46..1b96909d3e 100644 --- a/app/views/admin/enterprise_groups/_form.html.haml +++ b/app/views/admin/enterprise_groups/_form.html.haml @@ -7,5 +7,9 @@ = render 'admin/shared/side_menu' .one.column   .eleven.columns.omega.fullwidth_inputs - = render 'inputs', f: f + = render 'form_primary_details', f: f + = render 'form_about', f: f + = render 'form_images', f: f + = render 'form_address', f: f + = render 'form_web', f: f = render "spree/admin/shared/#{action}_resource_links" diff --git a/app/views/admin/enterprise_groups/_form_about.html.haml b/app/views/admin/enterprise_groups/_form_about.html.haml new file mode 100644 index 0000000000..60d7276094 --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_about.html.haml @@ -0,0 +1,6 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } + %legend About + = f.field_container :long_description do + = f.label :long_description + %br/ + = f.text_area :long_description diff --git a/app/views/admin/enterprise_groups/_form_address.html.haml b/app/views/admin/enterprise_groups/_form_address.html.haml new file mode 100644 index 0000000000..081af7110e --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_address.html.haml @@ -0,0 +1,36 @@ += f.fields_for :address do |af| + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } + %legend Contact + .row + .alpha.three.columns + = af.label :phone + .omega.eight.columns + = af.text_field :phone, { placeholder: "eg. 98 7654 3210"} + .row + .three.columns.alpha + = af.label :address1 + .eight.columns.omega + = af.text_field :address1, { placeholder: "eg. 123 High Street"} + .row + .alpha.three.columns + = af.label :address2 + .eight.columns.omega + = af.text_field :address2 + .row + .three.columns.alpha + = af.label :city, 'Suburb' + \/ + = af.label :zipcode, 'Postcode' + .four.columns + = af.text_field :city, { placeholder: "eg. Northcote"} + .four.columns.omega + = af.text_field :zipcode, { placeholder: "eg. 3070"} + .row + .three.columns.alpha + = af.label :state_id, 'State' + \/ + = af.label :country_id, 'Country' + .four.columns + = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" + .four.columns.omega + = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" diff --git a/app/views/admin/enterprise_groups/_form_images.html.haml b/app/views/admin/enterprise_groups/_form_images.html.haml new file mode 100644 index 0000000000..49169851c3 --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_images.html.haml @@ -0,0 +1,18 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Images'" } } + %legend Images + .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/admin/enterprise_groups/_form_primary_details.html.haml b/app/views/admin/enterprise_groups/_form_primary_details.html.haml new file mode 100644 index 0000000000..27dec0701c --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_primary_details.html.haml @@ -0,0 +1,30 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Primary Details'" } } + %legend Primary Details + = f.field_container :name do + = f.label :name + %br/ + = f.text_field :name + + = f.field_container :description do + = f.label :description + %br/ + = f.text_field :description + + - if spree_current_user.admin? + .row + .three.columns.alpha + =f.label :owner_id, 'Owner' + .with-tip{'data-powertip' => "The primary user responsible for this group."} + %a What's this? + .eight.columns.omega + = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: @owner_email + + = f.field_container :on_front_page do + = f.label :on_front_page, 'On front page?' + %br/ + = f.check_box :on_front_page + + = f.field_container :enterprise_ids do + = f.label :enterprise_ids, 'Enterprises' + %br/ + = f.collection_select :enterprise_ids, @enterprises, :id, :name, {}, {class: "select2 fullwidth", multiple: true} diff --git a/app/views/admin/enterprise_groups/_form_web.html.haml b/app/views/admin/enterprise_groups/_form_web.html.haml new file mode 100644 index 0000000000..ea985259c3 --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_web.html.haml @@ -0,0 +1,32 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Web'" } } + %legend Web Resources + .row + .alpha.three.columns + = f.label :website + .omega.eight.columns + = f.text_field :website, { placeholder: "eg. www.truffles.com"} + .row + .alpha.three.columns + = f.label :email + .omega.eight.columns + = f.text_field :email + .row + .alpha.three.columns + = f.label :facebook, 'Facebook' + .omega.eight.columns + = f.text_field :facebook + .row + .alpha.three.columns + = f.label :instagram, 'Instagram' + .omega.eight.columns + = f.text_field :instagram + .row + .alpha.three.columns + = f.label :linkedin, 'LinkedIn' + .omega.eight.columns + = f.text_field :linkedin + .row + .alpha.three.columns + = f.label :twitter + .omega.eight.columns + = f.text_field :twitter, { placeholder: "eg. @the_prof" } diff --git a/app/views/admin/enterprise_groups/_inputs.html.haml b/app/views/admin/enterprise_groups/_inputs.html.haml deleted file mode 100644 index dbf8606af6..0000000000 --- a/app/views/admin/enterprise_groups/_inputs.html.haml +++ /dev/null @@ -1,127 +0,0 @@ -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Primary Details'" } } - %legend Primary Details - = f.field_container :name do - = f.label :name - %br/ - = f.text_field :name - - = f.field_container :description do - = f.label :description - %br/ - = f.text_field :description - - - if spree_current_user.admin? - .row - .three.columns.alpha - =f.label :owner_id, 'Owner' - .with-tip{'data-powertip' => "The primary user responsible for this group."} - %a What's this? - .eight.columns.omega - = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: @owner_email - - = f.field_container :on_front_page do - = f.label :on_front_page, 'On front page?' - %br/ - = f.check_box :on_front_page - - = f.field_container :enterprise_ids do - = f.label :enterprise_ids, 'Enterprises' - %br/ - = f.collection_select :enterprise_ids, @enterprises, :id, :name, {}, {class: "select2 fullwidth", multiple: true} - -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } - %legend About - = f.field_container :long_description do - = f.label :long_description - %br/ - = f.text_area :long_description - - -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Images'" } } - %legend Images - .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 - -= f.fields_for :address do |af| - %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } - %legend Contact - .row - .alpha.three.columns - = af.label :phone - .omega.eight.columns - = af.text_field :phone, { placeholder: "eg. 98 7654 3210"} - .row - .three.columns.alpha - = af.label :address1 - .eight.columns.omega - = af.text_field :address1, { placeholder: "eg. 123 High Street"} - .row - .alpha.three.columns - = af.label :address2 - .eight.columns.omega - = af.text_field :address2 - .row - .three.columns.alpha - = af.label :city, 'Suburb' - \/ - = af.label :zipcode, 'Postcode' - .four.columns - = af.text_field :city, { placeholder: "eg. Northcote"} - .four.columns.omega - = af.text_field :zipcode, { placeholder: "eg. 3070"} - .row - .three.columns.alpha - = af.label :state_id, 'State' - \/ - = af.label :country_id, 'Country' - .four.columns - = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" - .four.columns.omega - = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" - -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Web'" } } - %legend Web Resources - .row - .alpha.three.columns - = f.label :website - .omega.eight.columns - = f.text_field :website, { placeholder: "eg. www.truffles.com"} - .row - .alpha.three.columns - = f.label :email - .omega.eight.columns - = f.text_field :email - .row - .alpha.three.columns - = f.label :facebook, 'Facebook' - .omega.eight.columns - = f.text_field :facebook - .row - .alpha.three.columns - = f.label :instagram, 'Instagram' - .omega.eight.columns - = f.text_field :instagram - .row - .alpha.three.columns - = f.label :linkedin, 'LinkedIn' - .omega.eight.columns - = f.text_field :linkedin - .row - .alpha.three.columns - = f.label :twitter - .omega.eight.columns - = f.text_field :twitter, { placeholder: "eg. @the_prof" } From f8ca24c5cdc92b94c1d34aefc58e1db6ddfa5643 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 20 Feb 2015 14:56:16 +1100 Subject: [PATCH 665/681] Index enterprise_groups address_id --- ...150220035501_add_address_id_index_to_enterprise_groups.rb | 5 +++++ db/schema.rb | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20150220035501_add_address_id_index_to_enterprise_groups.rb diff --git a/db/migrate/20150220035501_add_address_id_index_to_enterprise_groups.rb b/db/migrate/20150220035501_add_address_id_index_to_enterprise_groups.rb new file mode 100644 index 0000000000..32094cbde7 --- /dev/null +++ b/db/migrate/20150220035501_add_address_id_index_to_enterprise_groups.rb @@ -0,0 +1,5 @@ +class AddAddressIdIndexToEnterpriseGroups < ActiveRecord::Migration + def change + add_index :enterprise_groups, :address_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 959e0f06d6..a8ac91e6d8 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 => 20150219021742) do +ActiveRecord::Schema.define(:version => 20150220035501) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -207,6 +207,7 @@ ActiveRecord::Schema.define(:version => 20150219021742) do t.integer "owner_id" end + add_index "enterprise_groups", ["address_id"], :name => "index_enterprise_groups_on_address_id" add_index "enterprise_groups", ["owner_id"], :name => "index_enterprise_groups_on_owner_id" create_table "enterprise_groups_enterprises", :id => false, :force => true do |t| @@ -584,9 +585,9 @@ ActiveRecord::Schema.define(:version => 20150219021742) do t.string "email" t.text "special_instructions" t.integer "distributor_id" + t.integer "order_cycle_id" t.string "currency" t.string "last_ip_address" - t.integer "order_cycle_id" t.integer "cart_id" end From 60b7a571c54c21085ba25e3a6f64570dc2ede71d Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 20 Feb 2015 15:35:07 +1100 Subject: [PATCH 666/681] Tweak styling and markup for contact info in right column. --- app/views/groups/_contact.html.haml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index c3da5e26ba..8b45ca8437 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -17,25 +17,25 @@ %div{bindonce: true} - if @group.facebook.present? || @group.twitter.present? || @group.linkedin.present? || @group.instagram.present? - %div.modal-centered + %div.modal-centered.pad-top %p.modal-header Follow .follow-icons{bindonce: true} %span - =link_to_service "http://twitter.com/", @group.twitter do - %i.ofn-i_041-twitter + =link_to_service "http://twitter.com/", @group.twitter do + %i.ofn-i_041-twitter %span - =link_to_service "https://www.facebook.com/", @group.facebook do - %i.ofn-i_044-facebook + =link_to_service "https://www.facebook.com/", @group.facebook do + %i.ofn-i_044-facebook %span - =link_to_service "https://www.linkedin.com/in/", @group.linkedin do - %i.ofn-i_042-linkedin + =link_to_service "https://www.linkedin.com/in/", @group.linkedin do + %i.ofn-i_042-linkedin %span - =link_to_service "http://instagram.com/", @group.instagram do - %i.ofn-i_043-instagram + =link_to_service "http://instagram.com/", @group.instagram do + %i.ofn-i_043-instagram %div{bindonce: true} - if @group.address1.present? || @group.city.present? - %div.modal-centered + %div.modal-centered.pad-top %p.modal-header Address %p = @group.address1 From 3ab961a7e2282593e1307a65c4e0ab2e7f7963d6 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 20 Feb 2015 16:14:40 +1100 Subject: [PATCH 667/681] New mixin for gradients --- app/assets/stylesheets/darkswarm/mixins.sass | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/mixins.sass b/app/assets/stylesheets/darkswarm/mixins.sass index 00aa601b2f..6925e84f72 100644 --- a/app/assets/stylesheets/darkswarm/mixins.sass +++ b/app/assets/stylesheets/darkswarm/mixins.sass @@ -120,5 +120,21 @@ background-repeat: no-repeat background-size: 100% auto - +@mixin gradient($gradient-clr1, $gradient-clr2) + background: $gradient-clr1 + // Old browsers + background: -moz-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%) + // FF3.6+ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, $gradient-clr1), color-stop(100%, $gradient-clr2)) + // Chrome,Safari4+ + background: -webkit-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%) + // Chrome10+,Safari5.1+ + background: -o-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%) + // Opera 11.10+ + background: -ms-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%) + // IE10+ + background: linear-gradient(to bottom, $gradient-clr1 0%, $gradient-clr2 100%) + // W3C + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$gradient-clr1', endColorstr='$gradient-clr2',GradientType=0 ) + // IE6-8 From 3aa06ee1e4ed50379b177d5121ab554546ddfa9e Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 20 Feb 2015 16:14:50 +1100 Subject: [PATCH 668/681] Tabs styling for groups page - making it pretty. --- app/assets/stylesheets/darkswarm/groups.css.sass | 15 +++++++++++---- app/views/groups/show.html.haml | 6 +++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 4b8e9f0540..6b5361c500 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -40,25 +40,32 @@ // Tabs .tabs dd a // Mobile first - padding: 0.35rem 0.5rem + padding: 0.25rem 0.45rem 0rem font-size: 0.75rem border: none margin-bottom: -2px margin-right: 2px + text-transform: capitalize + @include avenir + @include border-radius(1em 0.25em 0 0) + @include gradient($disabled-light, $disabled-bright) @media screen and (min-width: 768px) .tabs dd a - padding: 0.45rem 0.75rem + padding: 0.5rem 1rem 0.25em font-size: 0.875rem + @include border-radius(1.5em 0.25em 0 0) @media screen and (min-width: 1024px) .tabs dd a - padding: 1rem 2rem + padding: 0.75rem 1.5rem 0.5em font-size: 1rem + @include border-radius(2em 0.25em 0 0) .tabs dd.active a + @include gradient(white, white) margin-bottom: -1px border-top: 1px solid $light-grey border-left: 1px solid $light-grey border-right: 1px solid $light-grey - border-bottom: 1px solid white + border-bottom: 0 .tabs-content border-top: 1px solid $light-grey border-left: 1px solid $light-grey diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 98de889532..94a54d59df 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -14,8 +14,8 @@ %h2.group-name= @group.name %p= @group.description - .small-12.columns.pad-top - .row.pad-top + .small-12.columns + .row .small-12.medium-12.large-9.columns %div{"ng-controller" => "TabsCtrl"} %tabset @@ -32,7 +32,7 @@ %tab{heading: 'About us', active: "active(\'about\')", select: "select(\'about\')"} - %h3.pad-top About us + %h1 About Us %p= @group.long_description %tab{heading: 'Our producers', From 985887334fa06ad9b227ce600f2dd3cdd641cd58 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 20 Feb 2015 16:43:03 +1100 Subject: [PATCH 669/681] Tweak the padding between header and tabs. --- app/views/groups/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 94a54d59df..dfa780f688 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -14,7 +14,7 @@ %h2.group-name= @group.name %p= @group.description - .small-12.columns + .small-12.columns.pad-top .row .small-12.medium-12.large-9.columns %div{"ng-controller" => "TabsCtrl"} From e05d64a0b43344fa7160e13940b24bde34f302f7 Mon Sep 17 00:00:00 2001 From: summerscope Date: Fri, 20 Feb 2015 16:43:36 +1100 Subject: [PATCH 670/681] WIP on groups index page. Needs work from Maikel or Rohan. --- .../stylesheets/darkswarm/groups.css.sass | 4 ++ app/views/groups/index.html.haml | 66 ++++++++++++------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 6b5361c500..039678de77 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -9,6 +9,10 @@ background-repeat: no-repeat padding-bottom: 20px + a > .group-name + &:hover, &:focus, &:active + text-decoration: underline + .group padding-bottom: 0.5em .row div diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 24b0389a89..d1013cfed4 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -7,36 +7,58 @@ #active-table-search.row.pad-top .small-12.columns %h1 Groups / regions - %p - %input.animate-show{type: :text, - "ng-model" => "query", - placeholder: "Search name or keyword", - "ng-debounce" => "150", - "ofn-disable-enter" => true} + / TODO: Maikel this search input still doesn't work. + / %p + / %input.animate-show{type: :text, + / "ng-model" => "query", + / placeholder: "Search name or keyword", + / "ng-debounce" => "150", + / "ofn-disable-enter" => true} .group{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", name: "group{{group.id}}", id: "group{{group.id}}"} .row.pad-top{bindonce: true} - .small-2.medium-1.columns - %h1 + .small-12.medium-6.columns + %a{"ng-href" => "groups/{{group.id}}"} %i.ofn-i_035-groups - .small-10.medium-11.columns - %h4 - %a{"ng-href" => "groups/{{group.id}}"} + %span.group-name {{ group.name }} + / %br + / %small + / %em + / {{ group.description }} + .small-4.medium-2.columns %p - %em - {{ group.description }} - /{{ group.state }} - /{{ group.email }} - /{{ group.twitter }} - / .small-12.medium-3.columns - / {{ group.address.state }} - / .small-6.columns.text-right - / %p - / %em - / {{ group.description }} + {{ group.state }} + .small-8.medium-4.columns.text-right + + / TODO: Maikel can we use the same logic on contacts partial to render these? doesn't work with the angular binding... + + / .follow-icons{bindonce: true} + / %span + / =link_to_service "http://twitter.com/", @group.twitter do + / %i.ofn-i_041-twitter + / %span + / =link_to_service "https://www.facebook.com/", @group.facebook do + / %i.ofn-i_044-facebook + / %span + / =link_to_service "https://www.linkedin.com/in/", @group.linkedin do + / %i.ofn-i_042-linkedin + / %span + / =link_to_service "http://instagram.com/", @group.instagram do + / %i.ofn-i_043-instagram + + %p + {{ group.facebook }} + %br + {{ group.twitter }} + %br + {{ group.instagram }} + %br + {{ group.email }} + %br + {{ group.website }} .group{"ng-show" => "groups.length == 0"} .row.pad-top From a522242e7a0c8ba6259076588ed500b9752d61df Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Sat, 21 Feb 2015 18:43:03 +1100 Subject: [PATCH 671/681] dealing with invalid live data in migration --- ...dd_address_instances_to_existing_enterprise_groups.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb b/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb index 0800948126..23fc038ce8 100644 --- a/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb +++ b/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb @@ -6,7 +6,14 @@ class AddAddressInstancesToExistingEnterpriseGroups < ActiveRecord::Migration next if g.address.present? address = Spree::Address.new(firstname: 'unused', lastname: 'unused', address1: 'undefined', city: 'undefined', zipcode: 'undefined', state: state, country: country, phone: 'undefined') g.address = address - g.save + # some groups are invalid, because of a missing description + g.save!(validate: false) end end + + def self.down + # we can't know which addresses were already there and which weren't + # and we can't remove addresses as long as they are referenced and + # required by the model + end end From eade6890704ed84a46ad26c2b27eb2ba21477521 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Sun, 22 Feb 2015 16:15:04 +1100 Subject: [PATCH 672/681] linkToService directive to generate external links --- .../directives/link_to_service.js.coffee | 8 +++++ .../darkswarm/filters/ext_url.js.coffee | 9 ++++++ app/views/groups/index.html.haml | 32 ++++--------------- 3 files changed, 23 insertions(+), 26 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/directives/link_to_service.js.coffee create mode 100644 app/assets/javascripts/darkswarm/filters/ext_url.js.coffee diff --git a/app/assets/javascripts/darkswarm/directives/link_to_service.js.coffee b/app/assets/javascripts/darkswarm/directives/link_to_service.js.coffee new file mode 100644 index 0000000000..dc0f513588 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/link_to_service.js.coffee @@ -0,0 +1,8 @@ +Darkswarm.directive "linkToService", -> + restrict: 'E' + replace: true + scope: { + ref: '=' + service: '=' + } + template: '' diff --git a/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee b/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee new file mode 100644 index 0000000000..8d4b8b90fe --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee @@ -0,0 +1,9 @@ +Darkswarm.filter "ext_url", () -> + urlPattern = /^https?:\/\// + (url, prefix) -> + if (!url) + return url + if (url.match(urlPattern)) + return url + else + return prefix + url diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index d1013cfed4..6115410b43 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -32,33 +32,13 @@ %p {{ group.state }} .small-8.medium-4.columns.text-right - - / TODO: Maikel can we use the same logic on contacts partial to render these? doesn't work with the angular binding... - - / .follow-icons{bindonce: true} - / %span - / =link_to_service "http://twitter.com/", @group.twitter do - / %i.ofn-i_041-twitter - / %span - / =link_to_service "https://www.facebook.com/", @group.facebook do - / %i.ofn-i_044-facebook - / %span - / =link_to_service "https://www.linkedin.com/in/", @group.linkedin do - / %i.ofn-i_042-linkedin - / %span - / =link_to_service "http://instagram.com/", @group.instagram do - / %i.ofn-i_043-instagram - %p - {{ group.facebook }} - %br - {{ group.twitter }} - %br - {{ group.instagram }} - %br - {{ group.email }} - %br - {{ group.website }} + %link-to-service.ofn-i_050-mail-circle{service: '""', ref: 'group.email.split("").reverse().join("")', mailto: true} + %link-to-service.ofn-i_049-web{service: '"http://"', ref: 'group.website'} + %link-to-service.ofn-i_041-twitter{service: '"http://twitter.com/"', ref: 'group.twitter'} + %link-to-service.ofn-i_044-facebook{service: '"https://www.facebook.com/"', ref: 'group.facebook'} + %link-to-service.ofn-i_042-linkedin{service: '"https://www.linkedin.com/in/"', ref: 'group.linkedin'} + %link-to-service.ofn-i_043-instagram{service: '"http://instagram.com/"', ref: 'group.instagram'} .group{"ng-show" => "groups.length == 0"} .row.pad-top From f8153c07b16ac3c476484a9206e6c7010f4e1876 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Sun, 22 Feb 2015 16:20:11 +1100 Subject: [PATCH 673/681] moving email from "web" to "contact" on group edit page --- app/views/admin/enterprise_groups/_form_address.html.haml | 5 +++++ app/views/admin/enterprise_groups/_form_web.html.haml | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/admin/enterprise_groups/_form_address.html.haml b/app/views/admin/enterprise_groups/_form_address.html.haml index 081af7110e..12bb7ef315 100644 --- a/app/views/admin/enterprise_groups/_form_address.html.haml +++ b/app/views/admin/enterprise_groups/_form_address.html.haml @@ -6,6 +6,11 @@ = af.label :phone .omega.eight.columns = af.text_field :phone, { placeholder: "eg. 98 7654 3210"} + .row + .alpha.three.columns + = f.label :email + .omega.eight.columns + = f.text_field :email .row .three.columns.alpha = af.label :address1 diff --git a/app/views/admin/enterprise_groups/_form_web.html.haml b/app/views/admin/enterprise_groups/_form_web.html.haml index ea985259c3..42638d94c6 100644 --- a/app/views/admin/enterprise_groups/_form_web.html.haml +++ b/app/views/admin/enterprise_groups/_form_web.html.haml @@ -5,11 +5,6 @@ = f.label :website .omega.eight.columns = f.text_field :website, { placeholder: "eg. www.truffles.com"} - .row - .alpha.three.columns - = f.label :email - .omega.eight.columns - = f.text_field :email .row .alpha.three.columns = f.label :facebook, 'Facebook' From fdde55f6319d26984bd8f86d5f247f4e100aee51 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 23 Feb 2015 10:03:03 +1100 Subject: [PATCH 674/681] Add spec for ext_url filter, refactor --- .../darkswarm/filters/ext_url.js.coffee | 10 ++++------ .../darkswarm/filters/ext_url_spec.js.coffee | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 spec/javascripts/unit/darkswarm/filters/ext_url_spec.js.coffee diff --git a/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee b/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee index 8d4b8b90fe..38eed7c294 100644 --- a/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee +++ b/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee @@ -1,9 +1,7 @@ -Darkswarm.filter "ext_url", () -> +Darkswarm.filter "ext_url", -> urlPattern = /^https?:\/\// (url, prefix) -> - if (!url) - return url - if (url.match(urlPattern)) - return url + if !url || url.match(urlPattern) + url else - return prefix + url + prefix + url diff --git a/spec/javascripts/unit/darkswarm/filters/ext_url_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/ext_url_spec.js.coffee new file mode 100644 index 0000000000..1575d8c246 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/filters/ext_url_spec.js.coffee @@ -0,0 +1,19 @@ +describe "ensuring absolute URL", -> + filter = null + + beforeEach -> + module 'Darkswarm' + inject ($filter) -> + filter = $filter 'ext_url' + + it "returns null when no URL given", -> + expect(filter(null, "http://")).toBeNull() + + it "returns the URL as-is for http URLs", -> + expect(filter("http://example.com", "http://")).toEqual "http://example.com" + + it "returns the URL as-is for https URLs", -> + expect(filter("https://example.com", "https://")).toEqual "https://example.com" + + it "returns with URL with prefix added when a relative URL is given", -> + expect(filter("example.com", "http://")).toEqual "http://example.com" From 9daf7e395597b93a35a1e7265b35b2a239e5f12b Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 25 Feb 2015 09:38:07 +1100 Subject: [PATCH 675/681] Removing spans on follow icons as these show up as blank spaces when no data to fill. --- app/views/groups/_contact.html.haml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index 8b45ca8437..915c608d15 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -19,19 +19,15 @@ - if @group.facebook.present? || @group.twitter.present? || @group.linkedin.present? || @group.instagram.present? %div.modal-centered.pad-top %p.modal-header Follow - .follow-icons{bindonce: true} - %span - =link_to_service "http://twitter.com/", @group.twitter do - %i.ofn-i_041-twitter - %span - =link_to_service "https://www.facebook.com/", @group.facebook do - %i.ofn-i_044-facebook - %span - =link_to_service "https://www.linkedin.com/in/", @group.linkedin do - %i.ofn-i_042-linkedin - %span - =link_to_service "http://instagram.com/", @group.instagram do - %i.ofn-i_043-instagram + .follow-icons{bindonce: true} + =link_to_service "http://twitter.com/", @group.twitter do + %i.ofn-i_041-twitter + =link_to_service "https://www.facebook.com/", @group.facebook do + %i.ofn-i_044-facebook + =link_to_service "https://www.linkedin.com/in/", @group.linkedin do + %i.ofn-i_042-linkedin + =link_to_service "http://instagram.com/", @group.instagram do + %i.ofn-i_043-instagram %div{bindonce: true} - if @group.address1.present? || @group.city.present? From 122cf6c0659d6bb2e7573b0897ec51d8820f6804 Mon Sep 17 00:00:00 2001 From: summerscope Date: Wed, 25 Feb 2015 10:22:33 +1100 Subject: [PATCH 676/681] Groups index page styling for responsive use cases. --- .../stylesheets/darkswarm/groups.css.sass | 17 ++++++++++- app/views/groups/index.html.haml | 29 +++++++++---------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 039678de77..7f70bf7211 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -13,12 +13,27 @@ &:hover, &:focus, &:active text-decoration: underline + .groups-icons + text-align: right + a + font-size: 1.5em + + .groups-header + border: 2px solid $clr-brick-light-bright + @include border-radius-mixed(0.5em, 0.5em, 0, 0) + margin: -1rem 0 1rem + padding: 1rem 0.9375rem + @media screen and (min-width: 640px) + border: 0 none + @include border-radius(0) + margin: 0 + padding: 0 + .group padding-bottom: 0.5em .row div font-size: 110% .row a - font-weight: 500 vertical-align: middle .ofn-i_035-groups font-size: 120% diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 6115410b43..5865823cd9 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -8,30 +8,27 @@ .small-12.columns %h1 Groups / regions / TODO: Maikel this search input still doesn't work. - / %p - / %input.animate-show{type: :text, - / "ng-model" => "query", - / placeholder: "Search name or keyword", - / "ng-debounce" => "150", - / "ofn-disable-enter" => true} + %p + %input.animate-show{type: :text, + "ng-model" => "query", + placeholder: "Search name or keyword", + "ng-debounce" => "150", + "ofn-disable-enter" => true} .group{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", name: "group{{group.id}}", id: "group{{group.id}}"} .row.pad-top{bindonce: true} .small-12.medium-6.columns - %a{"ng-href" => "groups/{{group.id}}"} - %i.ofn-i_035-groups - %span.group-name - {{ group.name }} - / %br - / %small - / %em - / {{ group.description }} - .small-4.medium-2.columns + .groups-header + %a{"ng-href" => "groups/{{group.id}}"} + %i.ofn-i_035-groups + %span.group-name + {{ group.name }} + .small-3.medium-2.columns %p {{ group.state }} - .small-8.medium-4.columns.text-right + .small-9.medium-4.columns.groups-icons %p %link-to-service.ofn-i_050-mail-circle{service: '""', ref: 'group.email.split("").reverse().join("")', mailto: true} %link-to-service.ofn-i_049-web{service: '"http://"', ref: 'group.website'} From 1f7aec0c1d7f56d292a5ff3ba38b8595ffd75d43 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 26 Feb 2015 13:09:46 +1100 Subject: [PATCH 677/681] group owner select box in own tab --- .../controllers/side_menu_controller.js.coffee | 1 + app/views/admin/enterprise_groups/_form.html.haml | 1 + .../_form_primary_details.html.haml | 9 --------- .../admin/enterprise_groups/_form_users.html.haml | 14 ++++++++++++++ 4 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 app/views/admin/enterprise_groups/_form_users.html.haml diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee index 63f05a9376..7b9a8165a1 100644 --- a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee @@ -5,6 +5,7 @@ angular.module("admin.enterprise_groups") $scope.menu.setItems [ { name: 'Primary Details', icon_class: "icon-user" } + { name: 'Users', icon_class: "icon-user" } { name: 'About', icon_class: "icon-pencil" } { name: 'Images', icon_class: "icon-picture" } { name: 'Contact', icon_class: "icon-phone" } diff --git a/app/views/admin/enterprise_groups/_form.html.haml b/app/views/admin/enterprise_groups/_form.html.haml index 1b96909d3e..39a4b8bd1e 100644 --- a/app/views/admin/enterprise_groups/_form.html.haml +++ b/app/views/admin/enterprise_groups/_form.html.haml @@ -8,6 +8,7 @@ .one.column   .eleven.columns.omega.fullwidth_inputs = render 'form_primary_details', f: f + = render 'form_users', f: f = render 'form_about', f: f = render 'form_images', f: f = render 'form_address', f: f diff --git a/app/views/admin/enterprise_groups/_form_primary_details.html.haml b/app/views/admin/enterprise_groups/_form_primary_details.html.haml index 27dec0701c..6d326e33fa 100644 --- a/app/views/admin/enterprise_groups/_form_primary_details.html.haml +++ b/app/views/admin/enterprise_groups/_form_primary_details.html.haml @@ -10,15 +10,6 @@ %br/ = f.text_field :description - - if spree_current_user.admin? - .row - .three.columns.alpha - =f.label :owner_id, 'Owner' - .with-tip{'data-powertip' => "The primary user responsible for this group."} - %a What's this? - .eight.columns.omega - = f.hidden_field :owner_id, class: "select2 fullwidth", 'ofn-user-autocomplete' => true, email: @owner_email - = f.field_container :on_front_page do = f.label :on_front_page, 'On front page?' %br/ diff --git a/app/views/admin/enterprise_groups/_form_users.html.haml b/app/views/admin/enterprise_groups/_form_users.html.haml new file mode 100644 index 0000000000..0a8a5dd635 --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_users.html.haml @@ -0,0 +1,14 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Users'" } } + %legend Users + .row + .three.columns.alpha + =f.label :owner_id, 'Owner' + .with-tip{'data-powertip' => "The primary user responsible for this group."} + %a What's this? + .eight.columns.omega + - if spree_current_user.admin? + = f.hidden_field :owner_id, + class: "select2 fullwidth", + 'user-select' => "{id:'#{@enterprise_group.owner.andand.id}', email:'#{@enterprise_group.owner.andand.email}'}" + - else + = @enterprise_group.owner.andand.email From fa4741eb658cb4656d62eef127e62dcb02fd4614 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 26 Feb 2015 13:47:40 +1100 Subject: [PATCH 678/681] Update auto-creation of E2E links: more specific and restricted link creation --- app/models/enterprise.rb | 31 ++++++++---- spec/models/enterprise_spec.rb | 86 ++++++++++++++++++++++------------ 2 files changed, 76 insertions(+), 41 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 8f5c940e77..462910aa0d 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -253,6 +253,10 @@ class Enterprise < ActiveRecord::Base self.sells != "none" end + def is_hub + self.sells == 'any' + end + # Simplify enterprise categories for frontend logic and icons, and maybe other things. def category # Make this crazy logic human readable so we can argue about it sanely. @@ -366,26 +370,33 @@ class Enterprise < ActiveRecord::Base end def relate_to_owners_enterprises - # When a new enterprise is created, we relate them to all enterprises owned by - # the same owner, in both directions. So all enterprises owned by the same owner - # will have permissions to every other one, in both directions. + # When a new producer is created, it grants permissions to all pre-existing hubs + # When a new hub is created, + # - it grants permissions to all pre-existing hubs + # - all producers grant permission to it enterprises = owner.owned_enterprises.where('enterprises.id != ?', self) - enterprises.each do |enterprise| + # We grant permissions to all pre-existing hubs + enterprises.is_hub.each do |enterprise| EnterpriseRelationship.create!(parent: self, child: enterprise, permissions_list: [:add_to_order_cycle, :manage_products, :edit_profile, :create_variant_overrides]) + end - EnterpriseRelationship.create!(parent: enterprise, - child: self, - permissions_list: [:add_to_order_cycle, - :manage_products, - :edit_profile, - :create_variant_overrides]) + # All pre-existing producers grant permission to new hubs + if is_hub + enterprises.is_primary_producer.each do |enterprise| + EnterpriseRelationship.create!(parent: enterprise, + child: self, + permissions_list: [:add_to_order_cycle, + :manage_products, + :edit_profile, + :create_variant_overrides]) + end end end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 494a543260..365bfd6ab0 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -594,46 +594,70 @@ describe Enterprise do describe "callbacks" do describe "after creation" do let(:owner) { create(:user, enterprise_limit: 10) } - let(:hub1) { create(:distributor_enterprise, owner: owner) } let(:hub2) { create(:distributor_enterprise, owner: owner) } - let(:producer) { create(:supplier_enterprise, owner: owner) } + let(:hub3) { create(:distributor_enterprise, owner: owner) } + let(:producer1) { create(:supplier_enterprise, owner: owner) } + let(:producer2) { create(:supplier_enterprise, owner: owner) } - let(:er1) { EnterpriseRelationship.where(child_id: hub1).last } - let(:er2) { EnterpriseRelationship.where(child_id: hub2).last } - let(:er3) { EnterpriseRelationship.where(child_id: producer).last } - let(:er4) { EnterpriseRelationship.where(parent_id: hub1).last } - let(:er5) { EnterpriseRelationship.where(parent_id: hub2).last } - let(:er6) { EnterpriseRelationship.where(parent_id: producer).last } - - it "establishes bi-directional relationships for new hubs with the owner's hubs and producers" do - hub1 - hub2 - producer - enterprise = nil - - expect do - enterprise = create(:enterprise, owner: owner) - end.to change(EnterpriseRelationship, :count).by(6) - - [er1, er2, er3].each do |er| - er.parent.should == enterprise - er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort + describe "when a producer is created" do + before do + hub1 + hub2 end - [er4, er5, er6].each do |er| - er.child.should == enterprise - er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort + it "creates links from the new producer to all hubs owned by the same user, granting all permissions" do + producer1 + + should_have_enterprise_relationship from: producer1, to: hub1, with: :all_permissions + should_have_enterprise_relationship from: producer1, to: hub2, with: :all_permissions + end + + it "does not create any other links" do + expect do + producer1 + end.to change(EnterpriseRelationship, :count).by(2) end end - it "establishes bi-directional relationships when producers are created" do - hub1 - hub2 - expect do - producer - end.to change(EnterpriseRelationship, :count).by(4) + describe "when a new hub is created" do + it "it creates links to the hub, from all producers owned by the same user, granting all permissions" do + producer1 + producer2 + hub1 + + should_have_enterprise_relationship from: producer1, to: hub1, with: :all_permissions + should_have_enterprise_relationship from: producer2, to: hub1, with: :all_permissions + end + + + it "creates links from the new hub to all hubs owned by the same user, granting all permissions" do + hub1 + hub2 + hub3 + + should_have_enterprise_relationship from: hub2, to: hub1, with: :all_permissions + should_have_enterprise_relationship from: hub3, to: hub1, with: :all_permissions + should_have_enterprise_relationship from: hub3, to: hub2, with: :all_permissions + end + + it "does not create any other links" do + producer1 + producer2 + expect { hub1 }.to change(EnterpriseRelationship, :count).by(2) # 2 producer links + expect { hub2 }.to change(EnterpriseRelationship, :count).by(3) # 2 producer links + 1 hub link + expect { hub3 }.to change(EnterpriseRelationship, :count).by(4) # 2 producer links + 2 hub links + end + end + + + def should_have_enterprise_relationship(opts={}) + er = EnterpriseRelationship.where(parent_id: opts[:from], child_id: opts[:to]).last + er.should_not be_nil + if opts[:with] == :all_permissions + er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort + end end end end From 379b702b9be8da06b799531b74d8c37eb0f9c11e Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 26 Feb 2015 16:22:15 +1100 Subject: [PATCH 679/681] spec: testing array of owned groups without order --- spec/models/spree/user_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 8510b89e0e..39692c2029 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -33,8 +33,8 @@ describe Spree.user_class do let!(:g3) { create(:enterprise_group, owner: u2) } it "provides access to owned groups" do - expect(u1.owned_groups(:reload)).to eq [g1, g2] - expect(u2.owned_groups(:reload)).to eq [g3] + expect(u1.owned_groups(:reload)).to match_array([g1, g2]) + expect(u2.owned_groups(:reload)).to match_array([g3]) end end From ff4bd449a23af142fd54ad4092a69548f103df03 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 26 Feb 2015 16:25:18 +1100 Subject: [PATCH 680/681] Setting default_country_id by application.yml The spree default_country_id was set using ENV["DEFAULT_COUNTRY"] for production environment, but not for tests and development. Since tests reset the default_country_id in specs/support/seeds.rb, only the development environment had a fix id set to 12. This is removed now. This fixes creating enterprises and enterprise groups without sample data (12 is Australia). --- config/initializers/spree.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index dca89955b7..5912e0b9ec 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -14,13 +14,8 @@ Spree.config do |config| config.checkout_zone = ENV["CHECKOUT_ZONE"] config.address_requires_state = true - # 12 should be Australia. Hardcoded for CI (Jenkins), where countries are not pre-loaded. - if Rails.env.test? or Rails.env.development? - config.default_country_id = 12 - else - country = Spree::Country.find_by_name(ENV["DEFAULT_COUNTRY"]) - config.default_country_id = country.id if country.present? - end + country = Spree::Country.find_by_name(ENV["DEFAULT_COUNTRY"]) + config.default_country_id = country.id if country.present? # -- spree_paypal_express # Auto-capture payments. Without this option, payments must be manually captured in the paypal interface. From 69fd3f0b602715ecabce35b768fc34226424c150 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 26 Feb 2015 16:41:51 +1100 Subject: [PATCH 681/681] Fix link to group pages if someone enters "/groups/" instead of "/group" --- app/views/groups/index.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 5865823cd9..262e188e3c 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -7,7 +7,6 @@ #active-table-search.row.pad-top .small-12.columns %h1 Groups / regions - / TODO: Maikel this search input still doesn't work. %p %input.animate-show{type: :text, "ng-model" => "query", @@ -21,7 +20,7 @@ .row.pad-top{bindonce: true} .small-12.medium-6.columns .groups-header - %a{"ng-href" => "groups/{{group.id}}"} + %a{"ng-href" => "/groups/{{group.id}}"} %i.ofn-i_035-groups %span.group-name {{ group.name }}