From 09086b8dd8e2871cada993203cacf5d6a834b5cc Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 18 Dec 2015 16:17:58 +1100 Subject: [PATCH 01/54] Reference implementation of SSO from Discourse --- lib/discourse/single_sign_on.rb | 107 ++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 lib/discourse/single_sign_on.rb diff --git a/lib/discourse/single_sign_on.rb b/lib/discourse/single_sign_on.rb new file mode 100644 index 0000000000..2dc323c7f5 --- /dev/null +++ b/lib/discourse/single_sign_on.rb @@ -0,0 +1,107 @@ +# This class is the reference implementation of a SSO provider from Discourse. + +module OpenFoodNetwork + class SingleSignOn + ACCESSORS = [:nonce, :name, :username, :email, :avatar_url, :avatar_force_update, :require_activation, + :about_me, :external_id, :return_sso_url, :admin, :moderator, :suppress_welcome_message] + FIXNUMS = [] + BOOLS = [:avatar_force_update, :admin, :moderator, :require_activation, :suppress_welcome_message] + NONCE_EXPIRY_TIME = 10.minutes + + attr_accessor(*ACCESSORS) + attr_accessor :sso_secret, :sso_url + + def self.sso_secret + raise RuntimeError, "sso_secret not implemented on class, be sure to set it on instance" + end + + def self.sso_url + raise RuntimeError, "sso_url not implemented on class, be sure to set it on instance" + end + + def self.parse(payload, sso_secret = nil) + sso = new + sso.sso_secret = sso_secret if sso_secret + + parsed = Rack::Utils.parse_query(payload) + if sso.sign(parsed["sso"]) != parsed["sig"] + diags = "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}" + if parsed["sso"] =~ /[^a-zA-Z0-9=\r\n\/+]/m + raise RuntimeError, "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}" + else + raise RuntimeError, "Bad signature for payload #{diags}" + end + end + + decoded = Base64.decode64(parsed["sso"]) + decoded_hash = Rack::Utils.parse_query(decoded) + + ACCESSORS.each do |k| + val = decoded_hash[k.to_s] + val = val.to_i if FIXNUMS.include? k + if BOOLS.include? k + val = ["true", "false"].include?(val) ? val == "true" : nil + end + sso.send("#{k}=", val) + end + + decoded_hash.each do |k,v| + # 1234567 + # custom. + # + if k[0..6] == "custom." + field = k[7..-1] + sso.custom_fields[field] = v + end + end + + sso + end + + def sso_secret + @sso_secret || self.class.sso_secret + end + + def sso_url + @sso_url || self.class.sso_url + end + + def custom_fields + @custom_fields ||= {} + end + + + def sign(payload) + OpenSSL::HMAC.hexdigest("sha256", sso_secret, payload) + end + + + def to_url(base_url=nil) + base = "#{base_url || sso_url}" + "#{base}#{base.include?('?') ? '&' : '?'}#{payload}" + end + + def payload + payload = Base64.encode64(unsigned_payload) + "sso=#{CGI::escape(payload)}&sig=#{sign(payload)}" + end + + def unsigned_payload + payload = {} + ACCESSORS.each do |k| + next if (val = send k) == nil + + payload[k] = val + end + + if @custom_fields + @custom_fields.each do |k,v| + payload["custom.#{k}"] = v.to_s + end + end + + Rack::Utils.build_query(payload) + end + + end +end From 030f4f63eda5dec4f7c2bfab74b486ceb17a021c Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 7 Jan 2016 12:23:39 +1100 Subject: [PATCH 02/54] SingleSignOn controller with routes and config --- app/controllers/discourse_sso_controller.rb | 38 +++++++++++++++++++++ config/application.yml.example | 4 +++ config/routes.rb | 2 ++ lib/discourse/single_sign_on.rb | 2 +- 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 app/controllers/discourse_sso_controller.rb diff --git a/app/controllers/discourse_sso_controller.rb b/app/controllers/discourse_sso_controller.rb new file mode 100644 index 0000000000..1aa18969e7 --- /dev/null +++ b/app/controllers/discourse_sso_controller.rb @@ -0,0 +1,38 @@ +require 'discourse/single_sign_on' + +class DiscourseSsoController < ApplicationController + include SharedHelper + + def sso + if spree_current_user + begin + redirect_to sso_url + rescue TypeError + render text: "Bad SingleSignOn request.", status: :bad_request + end + else + redirect_to login_path + end + end + + def sso_url + secret = ENV['DISCOURSE_SSO_SECRET'] or raise 'Missing SSO secret' + discourse_url = ENV['DISCOURSE_SSO_URL'] or raise 'Missing Discourse SSO login URL.' + sso = Discourse::SingleSignOn.parse(request.query_string, secret) + sso.email = spree_current_user.email + sso.username = spree_current_user.login + sso.external_id = spree_current_user.id + sso.sso_secret = secret + sso.admin = admin_user? + sso.require_activation = require_activation? + sso.to_url(discourse_url) + end + + def require_activation? + !admin_user? && !email_validated? + end + + def email_validated? + spree_current_user.confirmed.map(&:email).include?(spree_current_user.email) + end +end diff --git a/config/application.yml.example b/config/application.yml.example index 45fface302..b76e8d2936 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -13,3 +13,7 @@ LOCALE: en CHECKOUT_ZONE: Australia # Find currency codes at http://en.wikipedia.org/wiki/ISO_4217. CURRENCY: AUD + +# SingleSignOn login for Discourse +#DISCOURSE_SSO_SECRET: "" +#DISCOURSE_SSO_URL: "https://community.openfoodnetwork.org/session/sso_login" diff --git a/config/routes.rb b/config/routes.rb index c8771560e9..229357276f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,8 @@ Openfoodnetwork::Application.routes.draw do get "/#/login", to: "home#index", as: :spree_login get "/login", to: redirect("/#/login") + get "/sso", to: "discourse_sso#sso" + get "/map", to: "map#index", as: :map get "/register", to: "registration#index", as: :registration diff --git a/lib/discourse/single_sign_on.rb b/lib/discourse/single_sign_on.rb index 2dc323c7f5..046a2d677c 100644 --- a/lib/discourse/single_sign_on.rb +++ b/lib/discourse/single_sign_on.rb @@ -1,6 +1,6 @@ # This class is the reference implementation of a SSO provider from Discourse. -module OpenFoodNetwork +module Discourse class SingleSignOn ACCESSORS = [:nonce, :name, :username, :email, :avatar_url, :avatar_force_update, :require_activation, :about_me, :external_id, :return_sso_url, :admin, :moderator, :suppress_welcome_message] From 217fa9a57c5a3cd2920e1db9facaeb8396cab82a Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 7 Jan 2016 19:01:09 +1100 Subject: [PATCH 03/54] UI integration of Discourse login --- app/controllers/discourse_sso_controller.rb | 25 +++++++++++++++++---- app/helpers/discourse_helper.rb | 25 +++++++++++++++++++++ app/views/shared/_signed_in.html.haml | 6 +++++ config/application.yml.example | 2 +- config/locales/en.yml | 1 + config/routes.rb | 3 ++- 6 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 app/helpers/discourse_helper.rb diff --git a/app/controllers/discourse_sso_controller.rb b/app/controllers/discourse_sso_controller.rb index 1aa18969e7..0b067f6c92 100644 --- a/app/controllers/discourse_sso_controller.rb +++ b/app/controllers/discourse_sso_controller.rb @@ -2,6 +2,17 @@ require 'discourse/single_sign_on' class DiscourseSsoController < ApplicationController include SharedHelper + include DiscourseHelper + + before_filter :require_config + + def login + if require_activation? + redirect_to discourse_url + else + redirect_to discourse_login_url + end + end def sso if spree_current_user @@ -15,9 +26,11 @@ class DiscourseSsoController < ApplicationController end end + private + def sso_url - secret = ENV['DISCOURSE_SSO_SECRET'] or raise 'Missing SSO secret' - discourse_url = ENV['DISCOURSE_SSO_URL'] or raise 'Missing Discourse SSO login URL.' + secret = discourse_sso_secret! + discourse_url = discourse_url! sso = Discourse::SingleSignOn.parse(request.query_string, secret) sso.email = spree_current_user.email sso.username = spree_current_user.login @@ -25,7 +38,11 @@ class DiscourseSsoController < ApplicationController sso.sso_secret = secret sso.admin = admin_user? sso.require_activation = require_activation? - sso.to_url(discourse_url) + sso.to_url(discourse_sso_url) + end + + def require_config + raise ActionController::RoutingError.new('Not Found') unless discourse_configured? end def require_activation? @@ -33,6 +50,6 @@ class DiscourseSsoController < ApplicationController end def email_validated? - spree_current_user.confirmed.map(&:email).include?(spree_current_user.email) + spree_current_user.enterprises.confirmed.map(&:email).include?(spree_current_user.email) end end diff --git a/app/helpers/discourse_helper.rb b/app/helpers/discourse_helper.rb new file mode 100644 index 0000000000..1d813cf404 --- /dev/null +++ b/app/helpers/discourse_helper.rb @@ -0,0 +1,25 @@ +module DiscourseHelper + def discourse_configured? + discourse_url.present? + end + + def discourse_url + ENV['DISCOURSE_URL'] + end + + def discourse_login_url + discourse_url + '/login' + end + + def discourse_sso_url + discourse_url + '/session/sso_login' + end + + def discourse_url! + discourse_url or raise 'Missing Discourse URL' + end + + def discourse_sso_secret! + ENV['DISCOURSE_SSO_SECRET'] or raise 'Missing SSO secret' + end +end diff --git a/app/views/shared/_signed_in.html.haml b/app/views/shared/_signed_in.html.haml index c3a1bd8cc8..3126913ca0 100644 --- a/app/views/shared/_signed_in.html.haml +++ b/app/views/shared/_signed_in.html.haml @@ -1,3 +1,9 @@ +- if discourse_configured? + %li + %a{href: discourse_login_path, target: '_blank'} + %span.nav-primary + = t 'label_notices' + %li.has-dropdown.not-click %a{href: "#"} diff --git a/config/application.yml.example b/config/application.yml.example index b76e8d2936..8b72a2011d 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -16,4 +16,4 @@ CURRENCY: AUD # SingleSignOn login for Discourse #DISCOURSE_SSO_SECRET: "" -#DISCOURSE_SSO_URL: "https://community.openfoodnetwork.org/session/sso_login" +#DISCOURSE_URL: "https://community.openfoodnetwork.org" diff --git a/config/locales/en.yml b/config/locales/en.yml index 04d350153b..062c4c2a29 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -111,6 +111,7 @@ en: label_account: "Account" label_more: "More" label_less: "Show less" + label_notices: "Notices" items: "items" cart_headline: "Your shopping cart" diff --git a/config/routes.rb b/config/routes.rb index 229357276f..a5ceb35874 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,7 +11,8 @@ Openfoodnetwork::Application.routes.draw do get "/#/login", to: "home#index", as: :spree_login get "/login", to: redirect("/#/login") - get "/sso", to: "discourse_sso#sso" + get "/discourse/login", to: "discourse_sso#login" + get "/discourse/sso", to: "discourse_sso#sso" get "/map", to: "map#index", as: :map From 46382e669f57e7fc2dbb8e60a5d09b441c85d634 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 7 Jan 2016 22:44:03 +1100 Subject: [PATCH 04/54] more verbose example of Discourse config --- config/application.yml.example | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/application.yml.example b/config/application.yml.example index 8b72a2011d..0b1871466b 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -15,5 +15,9 @@ CHECKOUT_ZONE: Australia CURRENCY: AUD # SingleSignOn login for Discourse +# +# DISCOURSE_SSO_SECRET should be a random string. It must be the same as provided to your Discourse instance. #DISCOURSE_SSO_SECRET: "" -#DISCOURSE_URL: "https://community.openfoodnetwork.org" +# +# DISCOURSE_URL must be the URL of your Discourse instance. +#DISCOURSE_URL: "https://noticeboard.openfoodnetwork.org.au" From 38cac3a3c4f925b49f9ebf8155561dc2ad60e41e Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 19 Feb 2016 11:23:38 +1100 Subject: [PATCH 05/54] Explicitly referencing main_app.discourse_login_path Fixes #830. --- app/views/shared/_signed_in.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_signed_in.html.haml b/app/views/shared/_signed_in.html.haml index 3126913ca0..0af52d6d94 100644 --- a/app/views/shared/_signed_in.html.haml +++ b/app/views/shared/_signed_in.html.haml @@ -1,6 +1,6 @@ - if discourse_configured? %li - %a{href: discourse_login_path, target: '_blank'} + %a{href: main_app.discourse_login_path, target: '_blank'} %span.nav-primary = t 'label_notices' From 6ba534fb58b7568d7266c01e92e1741aabb7674b Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 19 Feb 2016 15:16:24 +1100 Subject: [PATCH 06/54] Show noticeboard in mobile menu --- app/views/shared/_signed_in_offcanvas.html.haml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/views/shared/_signed_in_offcanvas.html.haml b/app/views/shared/_signed_in_offcanvas.html.haml index 839d1c08fe..f34a03068f 100644 --- a/app/views/shared/_signed_in_offcanvas.html.haml +++ b/app/views/shared/_signed_in_offcanvas.html.haml @@ -1,3 +1,10 @@ +- if discourse_configured? + %li.li-menu + %a{href: main_app.discourse_login_path, target: '_blank'} + %span.nav-primary + %i.ofn-i_025-notepad + = t 'label_notices' + - if admin_user? or enterprise_user? %li %a{href: spree.admin_path, target:'_blank'} From 0cb5dfbbe03944c205d9caad92241e056219b610 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 19 Feb 2016 16:29:19 +1100 Subject: [PATCH 07/54] delete old sidebar partials --- app/views/shared/_account_sidebar.html.haml | 20 -------------------- app/views/shared/_sidebar.html.haml | 12 ------------ 2 files changed, 32 deletions(-) delete mode 100644 app/views/shared/_account_sidebar.html.haml delete mode 100644 app/views/shared/_sidebar.html.haml diff --git a/app/views/shared/_account_sidebar.html.haml b/app/views/shared/_account_sidebar.html.haml deleted file mode 100644 index e572979eb1..0000000000 --- a/app/views/shared/_account_sidebar.html.haml +++ /dev/null @@ -1,20 +0,0 @@ --##account{"ng-controller" => "AccountSidebarCtrl"} - -#.row - -#.panel - -#%p - -#%strong= link_to "Manage my account", account_path - -#- if enterprise_user? - -#%strong= link_to "Enterprise admin", admin_path - -#- if order = last_completed_order - -#%dl - -#%dt Current Hub: - -#%dd= link_to current_distributor.name, main_app.shop_path - -#%br - -#%dt Last hub: - -#%dd - -#- if order.distributor != current_distributor - -#= link_to "#{order.distributor.name}".html_safe, "", - -#{class: distributor_link_class(order.distributor), - -#"ng-click" => "emptyCart('#{main_app.enterprise_shop_path(order.distributor)}', $event)"} - -#- else - -#= order.distributor.name diff --git a/app/views/shared/_sidebar.html.haml b/app/views/shared/_sidebar.html.haml deleted file mode 100644 index 8fdcf7c2b5..0000000000 --- a/app/views/shared/_sidebar.html.haml +++ /dev/null @@ -1,12 +0,0 @@ --#%aside#sidebar.right-off-canvas-menu{ role: "complementary", "ng-controller" => "SidebarCtrl", --#"ng-class" => "{'active' : Sidebar.active()}"} - - -#- if spree_current_user.nil? - -#%tabset - -#= render partial: "shared/login_sidebar" - -#= render partial: "shared/signup_sidebar" - -#= render partial: "shared/forgot_sidebar" - -#- else - -#= render partial: "shared/account_sidebar" - - -#= yield :sidebar From 6a2e07064b437e3e87d56a5e50ed18382aa4007a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Feb 2016 11:23:13 +1100 Subject: [PATCH 08/54] Fix stomped spree JS file --- app/assets/javascripts/admin/all.js | 2 +- .../javascripts/admin/{admin.js.coffee => ofn_admin.js.coffee} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/assets/javascripts/admin/{admin.js.coffee => ofn_admin.js.coffee} (100%) diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 7d3e94194d..b4bd1ca5ec 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -21,7 +21,7 @@ //= require ../shared/ng-tags-input.min.js //= require angular-rails-templates //= require_tree ../templates/admin -//= require ./admin +//= require ./ofn_admin //= require ./accounts_and_billing_settings/accounts_and_billing_settings //= require ./business_model_configuration/business_model_configuration //= require ./customers/customers diff --git a/app/assets/javascripts/admin/admin.js.coffee b/app/assets/javascripts/admin/ofn_admin.js.coffee similarity index 100% rename from app/assets/javascripts/admin/admin.js.coffee rename to app/assets/javascripts/admin/ofn_admin.js.coffee From d79a6d7e195f82af552e64fd8bd71754a7e6e9ff Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Feb 2016 11:24:42 +1100 Subject: [PATCH 09/54] Escape HTML entities in JSON --- config/application.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/application.rb b/config/application.rb index ba75a097ec..1fb6c88bbf 100644 --- a/config/application.rb +++ b/config/application.rb @@ -102,5 +102,6 @@ module Openfoodnetwork config.assets.precompile += ['search/all.css', 'search/*.js'] config.assets.precompile += ['shared/*'] + config.active_support.escape_html_entities_in_json = true end end From 45d4dd6b88e453a15f925edb054666107557d37a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Feb 2016 14:25:55 +1100 Subject: [PATCH 10/54] Add ofnWithTip directive to sanitise HTML going into tooltips --- .../javascripts/admin/utils/directives/with_tip.js.coffee | 8 ++++++++ app/assets/javascripts/admin/utils/utils.js.coffee | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/admin/utils/directives/with_tip.js.coffee diff --git a/app/assets/javascripts/admin/utils/directives/with_tip.js.coffee b/app/assets/javascripts/admin/utils/directives/with_tip.js.coffee new file mode 100644 index 0000000000..51bb5b05bf --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/with_tip.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.utils").directive "ofnWithTip", ($sanitize)-> + link: (scope, element, attrs) -> + element.attr('data-powertip', $sanitize(attrs.ofnWithTip)) + element.powerTip + smartPlacement: true + fadeInTime: 50 + fadeOutTime: 50 + intentPollInterval: 300 diff --git a/app/assets/javascripts/admin/utils/utils.js.coffee b/app/assets/javascripts/admin/utils/utils.js.coffee index 4d58ae930a..094d3a5849 100644 --- a/app/assets/javascripts/admin/utils/utils.js.coffee +++ b/app/assets/javascripts/admin/utils/utils.js.coffee @@ -1 +1 @@ -angular.module("admin.utils", []) \ No newline at end of file +angular.module("admin.utils", ["ngSanitize"]) \ No newline at end of file From e2722710dedd24c5a8cbafbd07588ff48a47dcbb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 11 Feb 2016 14:26:08 +1100 Subject: [PATCH 11/54] Clean up syntax --- .../controllers/enterprises_dashboard_controller.js.coffee | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee index ad72ff3529..60315d30c1 100644 --- a/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee @@ -1,5 +1,2 @@ -angular.module("ofn.admin").controller "enterprisesDashboardCtrl", [ - "$scope" - ($scope) -> - $scope.activeTab = "hubs" -] \ No newline at end of file +angular.module("ofn.admin").controller "enterprisesDashboardCtrl", ($scope) -> + $scope.activeTab = "hubs" From b4976a5445d2dba0c89ccfba615ec88514e470f3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Feb 2016 07:28:17 +1100 Subject: [PATCH 12/54] Replace with-tip with ofn-with-tip: enterprise forms --- app/assets/javascripts/admin/users/users.js.coffee | 2 +- app/views/admin/enterprises/_actions.html.haml | 6 +++--- app/views/admin/enterprises/_admin_index.html.haml | 2 +- app/views/admin/enterprises/_new_form.html.haml | 8 ++++---- app/views/admin/enterprises/form/_images.html.haml | 4 ++-- .../enterprises/form/_primary_details.html.haml | 12 ++++++------ app/views/admin/enterprises/form/_users.html.haml | 4 ++-- app/views/admin/enterprises/new.html.haml | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/admin/users/users.js.coffee b/app/assets/javascripts/admin/users/users.js.coffee index 6bfd47a894..a86a90a56c 100644 --- a/app/assets/javascripts/admin/users/users.js.coffee +++ b/app/assets/javascripts/admin/users/users.js.coffee @@ -1 +1 @@ -angular.module("admin.users", []) \ No newline at end of file +angular.module("admin.users", ['admin.utils']) \ No newline at end of file diff --git a/app/views/admin/enterprises/_actions.html.haml b/app/views/admin/enterprises/_actions.html.haml index ec29607747..5bcfc7a512 100644 --- a/app/views/admin/enterprises/_actions.html.haml +++ b/app/views/admin/enterprises/_actions.html.haml @@ -15,18 +15,18 @@ = 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"} + %span.icon-exclamation-sign{"ofn-with-tip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} %br/ - 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"} + %span.icon-exclamation-sign{"ofn-with-tip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} %br/ - 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 - %span.icon-warning-sign.with-tip{"data-powertip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} + %span.icon-warning-sign{"ofn-with-tip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} diff --git a/app/views/admin/enterprises/_admin_index.html.haml b/app/views/admin/enterprises/_admin_index.html.haml index 68c74a783c..2359f45f78 100644 --- a/app/views/admin/enterprises/_admin_index.html.haml +++ b/app/views/admin/enterprises/_admin_index.html.haml @@ -2,7 +2,7 @@ - if flash[:action] %p= flash[:action] -= 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, html: {"ng-app" => "admin.enterprises"} do |f| %table#listing_enterprises.index %colgroup %col{style: "width: 25%;"}/ diff --git a/app/views/admin/enterprises/_new_form.html.haml b/app/views/admin/enterprises/_new_form.html.haml index a76c8ec31c..3ab9392712 100644 --- a/app/views/admin/enterprises/_new_form.html.haml +++ b/app/views/admin/enterprises/_new_form.html.haml @@ -5,10 +5,10 @@ = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles", class: "fullwidth" } - if spree_current_user.admin? - .row{ ng: { app: "admin.users" } } + .row .three.columns.alpha =f.label :owner_id, 'Owner' - .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} + %div{'ofn-with-tip' => "The primary user responsible for this enterprise."} %a What's this? .nine.columns.omega - owner_email = @enterprise.andand.owner.andand.email || "" @@ -16,7 +16,7 @@ .row .three.columns.alpha %label Primary Producer? - .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %div{'ofn-with-tip' => "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' @@ -27,7 +27,7 @@ .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.
"} + %div{'ofn-with-tip' => "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' diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml index ac960ccc1e..c03be2caa3 100644 --- a/app/views/admin/enterprises/form/_images.html.haml +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -8,7 +8,7 @@ = f.file_field :logo .row .alpha.three.columns - = f.label :promo_image, class: 'with-tip', 'data-powertip' => 'This image is displayed in "About Us"' + = f.label :promo_image, 'ofn-with-tip' => '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. @@ -16,4 +16,4 @@ .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 + = f.file_field :promo_image diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml index 96f4674912..437a61e866 100644 --- a/app/views/admin/enterprises/form/_primary_details.html.haml +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -10,7 +10,7 @@ .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."} + %div{'ofn-with-tip' => "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, @groups, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..." @@ -18,7 +18,7 @@ .row .three.columns.alpha %label Primary Producer - .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %div{'ofn-with-tip' => "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' @@ -29,7 +29,7 @@ .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.
"} + %div{'ofn-with-tip' => "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' @@ -46,7 +46,7 @@ .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."} + %div{'ofn-with-tip' => "Determines whether this enterprise will be visible to customers when searching the site."} %a What's this? .two.columns = f.radio_button :visible, true @@ -60,7 +60,7 @@ .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"} + %div{'ofn-with-tip' => "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} }" } @@ -72,7 +72,7 @@ .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."} + %div{'ofn-with-tip' => "A direct link to your shopfront on the Open Food Network."} %a What's this? .eight.columns.omega = surround spree.root_url, "/shop" do diff --git a/app/views/admin/enterprises/form/_users.html.haml b/app/views/admin/enterprises/form/_users.html.haml index d13723795d..289a85d7a1 100644 --- a/app/views/admin/enterprises/form/_users.html.haml +++ b/app/views/admin/enterprises/form/_users.html.haml @@ -6,7 +6,7 @@ =f.label :owner_id, 'Owner' - if full_permissions %span.required * - .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} + %div{'ofn-with-tip' => "The primary user responsible for this enterprise."} %a What's this? .eight.columns.omega - if full_permissions @@ -19,7 +19,7 @@ =f.label :user_ids, 'Managers' - if full_permissions %span.required * - .with-tip{'data-powertip' => "The other users with permission to manage this enterprise."} + %div{'ofn-with-tip' => "The other users with permission to manage this enterprise."} %a What's this? .eight.columns.omega - if full_permissions diff --git a/app/views/admin/enterprises/new.html.haml b/app/views/admin/enterprises/new.html.haml index 8f21067941..4413acfc2c 100644 --- a/app/views/admin/enterprises/new.html.haml +++ b/app/views/admin/enterprises/new.html.haml @@ -11,5 +11,5 @@ = form_for [main_app, :admin, @enterprise], html: { "nav-check" => '', "nav-callback" => '' } do |f| .row - .twelve.columns.fullwidth_inputs + .twelve.columns.fullwidth_inputs{ ng: { app: "admin.users" } } = render 'new_form', f: f From d699f8321a53bc425a6807482b33403e85c011ae Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Feb 2016 07:48:35 +1100 Subject: [PATCH 13/54] Replace with-tip with ofn-with-tip: dashboard --- .../stylesheets/admin/dashboard_item.css.sass | 4 +-- .../admin/overview/_enterprises.html.haml | 2 +- .../overview/_enterprises_header.html.haml | 2 +- .../overview/_enterprises_hubs_tab.html.haml | 12 ++++---- .../admin/overview/_order_cycles.html.haml | 4 +-- .../spree/admin/overview/_products.html.haml | 4 +-- .../multi_enterprise_dashboard.html.haml | 29 ++++++++++--------- 7 files changed, 29 insertions(+), 28 deletions(-) diff --git a/app/assets/stylesheets/admin/dashboard_item.css.sass b/app/assets/stylesheets/admin/dashboard_item.css.sass index 13b16084f6..4bb4855660 100644 --- a/app/assets/stylesheets/admin/dashboard_item.css.sass +++ b/app/assets/stylesheets/admin/dashboard_item.css.sass @@ -25,7 +25,7 @@ div.dashboard_item border: 1px solid #5498da position: relative - a.with-tip + a[ofn-with-tip] position: absolute right: 5px bottom: 5px @@ -35,7 +35,7 @@ div.dashboard_item border-width: 3px h3 color: #DA5354 - + &.orange border-color: #DA7F52 border-width: 3px diff --git a/app/views/spree/admin/overview/_enterprises.html.haml b/app/views/spree/admin/overview/_enterprises.html.haml index 757755c718..fbf19fbd43 100644 --- a/app/views/spree/admin/overview/_enterprises.html.haml +++ b/app/views/spree/admin/overview/_enterprises.html.haml @@ -1,4 +1,4 @@ -%div.dashboard_item.sixteen.columns.alpha#enterprises{ 'ng-app' => 'ofn.admin', 'ng-controller' => "enterprisesDashboardCtrl" } +%div.dashboard_item.sixteen.columns.alpha#enterprises{ 'ng-controller' => "enterprisesDashboardCtrl" } = render 'enterprises_header' - if @enterprises.empty? diff --git a/app/views/spree/admin/overview/_enterprises_header.html.haml b/app/views/spree/admin/overview/_enterprises_header.html.haml index fcc7d269f2..d198e8b549 100644 --- a/app/views/spree/admin/overview/_enterprises_header.html.haml +++ b/app/views/spree/admin/overview/_enterprises_header.html.haml @@ -5,4 +5,4 @@ %a.three.columns.omega.icon-plus.button.blue.white-bottom{ href: "#{main_app.new_admin_enterprise_path}" } CREATE NEW - else - %a.with-tip{ title: "Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network." } What's this? + %a{ "ofn-with-tip" => "Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network." } What's this? diff --git a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml index cb177f9fb3..78c0e2427b 100644 --- a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml @@ -16,27 +16,27 @@ - if can? :admin, Spree::PaymentMethod - payment_method_count = enterprise.payment_methods.count - if payment_method_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{pluralize payment_method_count, 'payment method'}" } + %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize payment_method_count, 'payment method'}" } - else - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no payment methods" } + %span.icon-remove-sign{ 'ofn-with-tip' => "#{enterprise.name} has no payment methods" } - else   %span.symbol.three.columns.centered - if can? :admin, Spree::ShippingMethod - shipping_method_count = enterprise.shipping_methods.count - if shipping_method_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{pluralize shipping_method_count, 'shipping method'}" } + %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize shipping_method_count, 'shipping method'}" } - else - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no shipping methods" } + %span.icon-remove-sign{ 'ofn-with-tip' => "#{enterprise.name} has no shipping methods" } - else   %span.symbol.three.columns.centered - if can? :admin, EnterpriseFee - fee_count = enterprise.enterprise_fees.count - if fee_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{pluralize fee_count, 'fee'}" } + %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize fee_count, 'fee'}" } - else - %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no enterprise fees" } + %span.icon-warning-sign{ 'ofn-with-tip' => "#{enterprise.name} has no enterprise fees" } - else   %span.two.columns.omega.right diff --git a/app/views/spree/admin/overview/_order_cycles.html.haml b/app/views/spree/admin/overview/_order_cycles.html.haml index c1b7f90276..34b82c1f87 100644 --- a/app/views/spree/admin/overview/_order_cycles.html.haml +++ b/app/views/spree/admin/overview/_order_cycles.html.haml @@ -5,7 +5,7 @@ %a.three.columns.omega.icon-plus.button.blue{ href: "#{main_app.new_admin_order_cycle_path}" } CREATE NEW - else - %a.with-tip{ title: "Order cycles determine when and where your products are available to customers." } What's this? + %a{ "ofn-with-tip" => "Order cycles determine when and where your products are available to customers." } What's this? %div.seven.columns.alpha.list - if @order_cycle_count > 0 %div.seven.columns.alpha.list-item @@ -23,4 +23,4 @@ %span.icon-warning-sign %a.seven.columns.alpha.button.bottom.orange{ href: "#{main_app.admin_order_cycles_path}" } MANAGE ORDER CYCLES - %span.icon-arrow-right \ No newline at end of file + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_products.html.haml b/app/views/spree/admin/overview/_products.html.haml index 988e779398..24b60250c3 100644 --- a/app/views/spree/admin/overview/_products.html.haml +++ b/app/views/spree/admin/overview/_products.html.haml @@ -5,7 +5,7 @@ %a.three.columns.omega.icon-plus.button.blue{ href: "#{new_admin_product_path}" } CREATE NEW - else - %a.with-tip{ title: "The products that you sell through the Open Food Network." } What's this? + %a{ "ofn-with-tip" => "The products that you sell through the Open Food Network." } What's this? %div.seven.columns.alpha.list - if @product_count > 0 %div.seven.columns.alpha.list-item @@ -23,4 +23,4 @@ %span.icon-remove-sign %a.seven.columns.alpha.button.bottom.red{ href: "#{new_admin_product_path}" } CREATE A NEW PRODUCT - %span.icon-arrow-right \ No newline at end of file + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml index 420196d3ce..511718d3f4 100644 --- a/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml @@ -2,27 +2,28 @@ = render 'admin/shared/user_guide_link' -%h1{ :style => 'margin-bottom: 30px'} Dashboard +%div{ 'ng-app' => 'ofn.admin' } + %h1{ :style => 'margin-bottom: 30px' } Dashboard -- if @enterprises.unconfirmed.any? + - if @enterprises.unconfirmed.any? - = render partial: "unconfirmed" + = render partial: "unconfirmed" - %hr + %hr -- if @enterprises.empty? + - if @enterprises.empty? - = render partial: "enterprises" + = render partial: "enterprises" -- else + - else - - if can? :admin, Spree::Product - = render partial: "products" + - if can? :admin, Spree::Product + = render partial: "products" - %div.two.columns -   + %div.two.columns +   - - if can? :admin, OrderCycle - = render partial: "order_cycles" + - if can? :admin, OrderCycle + = render partial: "order_cycles" - = render partial: "enterprises" + = render partial: "enterprises" From bdd6d3ba6b860bc7f5a8c7ec28b75da989b3ce5c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Feb 2016 08:00:28 +1100 Subject: [PATCH 14/54] Replace with-tip with ofn-with-tip: business model configuration --- .../admin/add_app_wrapper.html.erb.deface | 5 ++++ .../edit.html.haml | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 app/overrides/spree/layouts/admin/add_app_wrapper.html.erb.deface diff --git a/app/overrides/spree/layouts/admin/add_app_wrapper.html.erb.deface b/app/overrides/spree/layouts/admin/add_app_wrapper.html.erb.deface new file mode 100644 index 0000000000..9c5e6fcd80 --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_app_wrapper.html.erb.deface @@ -0,0 +1,5 @@ + + +
> + <%= render_original %> +
diff --git a/app/views/admin/business_model_configuration/edit.html.haml b/app/views/admin/business_model_configuration/edit.html.haml index 09a5949456..89345178f9 100644 --- a/app/views/admin/business_model_configuration/edit.html.haml +++ b/app/views/admin/business_model_configuration/edit.html.haml @@ -1,12 +1,15 @@ = render :partial => 'spree/admin/shared/configuration_menu' +- content_for :app_wrapper_attrs do + = "ng-app='admin.businessModelConfiguration'" + - content_for :page_title do %h1.page-title= t(:business_model_configuration) - %a.with-tip{ 'data-powertip' => "Configure the rate at which shops will be charged each month for use of the Open Food Network." } What's this? + %a{ 'ofn-with-tip' => "Configure the rate at which shops will be charged each month for use of the Open Food Network." } What's this? = render 'spree/shared/error_messages', target: @settings -.row{ ng: { app: 'admin.businessModelConfiguration', controller: "BusinessModelConfigCtrl" } } +.row{ ng: { controller: "BusinessModelConfigCtrl" } } .five.columns.omega %fieldset.no-border-bottom %legend=t(:bill_calculation_settings) @@ -17,7 +20,7 @@ .row .three.columns.alpha = f.label :account_invoices_monthly_fixed, t(:fixed_monthly_charge) - %span.with-tip.icon-question-sign{'data-powertip' => "A fixed monthly charge for ALL enterprises who are set up as a shop, regardless of how much produce they sell."} + %span.icon-question-sign{'ofn-with-tip' => "A fixed monthly charge for ALL enterprises who are set up as a shop, regardless of how much produce they sell."} .two.columns.omega .input-symbol.before %span= Spree::Money.currency_symbol @@ -25,13 +28,13 @@ .row .three.columns.alpha = f.label :account_invoices_monthly_rate, t(:percentage_of_turnover) - %span.with-tip.icon-question-sign{'data-powertip' => "When greater than zero, this rate (0.0 - 1.0) will be applied to the total turnover of each shop and added to any fixed charges (to the left) to calculate the monthly bill."} + %span.icon-question-sign{'ofn-with-tip' => "When greater than zero, this rate (0.0 - 1.0) will be applied to the total turnover of each shop and added to any fixed charges (to the left) to calculate the monthly bill."} .two.columns.omega = f.number_field :account_invoices_monthly_rate, min: 0.0, max: 1.0, step: 0.01, class: "fullwidth", 'watch-value-as' => 'rate' .row .three.columns.alpha = f.label :account_invoices_monthly_cap, t(:monthly_cap_excl_tax) - %span.with-tip.icon-question-sign{'data-powertip' => "When greater than zero, this value will be used as a cap on the amount that shops will be charged each month."} + %span.icon-question-sign{'ofn-with-tip' => "When greater than zero, this value will be used as a cap on the amount that shops will be charged each month."} .two.columns.omega .input-symbol.before %span= Spree::Money.currency_symbol @@ -39,7 +42,7 @@ .row .three.columns.alpha = f.label :account_invoices_tax_rate, t(:tax_rate) - %span.with-tip.icon-question-sign{'data-powertip' => "Tax rate that applies to the the monthly bill that enterprises are charged for using the system."} + %span.icon-question-sign{'ofn-with-tip' => "Tax rate that applies to the the monthly bill that enterprises are charged for using the system."} .two.columns.omega = f.number_field :account_invoices_tax_rate, min: 0.0, max: 1.0, step: 0.01, class: "fullwidth", 'watch-value-as' => 'taxRate' @@ -59,7 +62,7 @@ .row .three.columns.alpha = label_tag :turnover, t(:example_monthly_turnover) - %span.with-tip.icon-question-sign{'data-powertip' => "An example monthly turnover for an enterprise which will be used to generate calculate an example monthly bill below."} + %span.icon-question-sign{'ofn-with-tip' => "An example monthly turnover for an enterprise which will be used to generate calculate an example monthly bill below."} .two.columns.omega .input-symbol.before %span= Spree::Money.currency_symbol @@ -67,18 +70,18 @@ .row .three.columns.alpha = label_tag :cap_reached, t(:cap_reached?) - %span.with-tip.icon-question-sign{'data-powertip' => "Whether the cap (specified to the left) has been reached, given the settings and the turnover provided."} + %span.icon-question-sign{'ofn-with-tip' => "Whether the cap (specified to the left) has been reached, given the settings and the turnover provided."} .two.columns.omega %input.fullwidth{ id: 'cap_reached', type: "text", readonly: true, ng: { value: 'capReached()' } } .row .three.columns.alpha = label_tag :included_tax, t(:included_tax) - %span.with-tip.icon-question-sign{'data-powertip' => "The total tax included in the example monthly bill, given the settings and the turnover provided."} + %span.icon-question-sign{'ofn-with-tip' => "The total tax included in the example monthly bill, given the settings and the turnover provided."} .two.columns.omega %input.fullwidth{ id: 'included_tax', type: "text", readonly: true, ng: { value: 'includedTax() | currency' } } .row .three.columns.alpha = label_tag :total_incl_tax, t(:total_monthly_bill_incl_tax) - %span.with-tip.icon-question-sign{'data-powertip' => "The example total monthly bill with tax included, given the settings and the turnover provided."} + %span.icon-question-sign{'ofn-with-tip' => "The example total monthly bill with tax included, given the settings and the turnover provided."} .two.columns.omega %input.fullwidth{ id: 'total_incl_tax', type: "text", readonly: true, ng: { value: 'total() | currency' } } From 17cda86dfa9d235c261063a5d69d9f74a83d2b08 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Feb 2016 08:06:38 +1100 Subject: [PATCH 15/54] Replace with-tip with ofn-with-tip: enterprise groups --- app/views/admin/enterprise_groups/_form_images.html.haml | 8 ++++---- app/views/admin/enterprise_groups/_form_users.html.haml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/admin/enterprise_groups/_form_images.html.haml b/app/views/admin/enterprise_groups/_form_images.html.haml index 49169851c3..1bfffa86d2 100644 --- a/app/views/admin/enterprise_groups/_form_images.html.haml +++ b/app/views/admin/enterprise_groups/_form_images.html.haml @@ -2,16 +2,16 @@ %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'} + = f.label :logo, 'ofn-with-tip' => 'This is the logo for the group' + %div{'ofn-with-tip' => 'This is the logo for the group'} %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'} + = f.label :promo_image, 'ofn-with-tip' => 'This image is displayed at the top of the Group profile' + %div{'ofn-with-tip' => '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? diff --git a/app/views/admin/enterprise_groups/_form_users.html.haml b/app/views/admin/enterprise_groups/_form_users.html.haml index 0a8a5dd635..c4f57da48a 100644 --- a/app/views/admin/enterprise_groups/_form_users.html.haml +++ b/app/views/admin/enterprise_groups/_form_users.html.haml @@ -3,7 +3,7 @@ .row .three.columns.alpha =f.label :owner_id, 'Owner' - .with-tip{'data-powertip' => "The primary user responsible for this group."} + %div{'ofn-with-tip' => "The primary user responsible for this group."} %a What's this? .eight.columns.omega - if spree_current_user.admin? From 258e84fc0a76a85c4670db4db320b13e75a41b00 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Feb 2016 08:33:37 +1100 Subject: [PATCH 16/54] Replace with-tip with ofn-with-tip: misc --- app/assets/javascripts/admin/enterprise_fees.js | 2 +- app/helpers/spree/admin/base_helper_decorator.rb | 2 +- .../image_settings/edit/add_image_format.html.haml.deface | 2 +- .../orders/index/add_special_instructions.html.haml.deface | 2 +- app/overrides/spree/admin/orders/index/set_ng_app.deface | 2 ++ app/views/admin/order_cycles/_row.html.haml | 4 ++-- app/views/admin/order_cycles/index.html.haml | 2 +- app/views/spree/admin/orders/bulk_management.html.haml | 7 +++++-- .../admin/products/bulk_edit/_products_variant.html.haml | 2 +- 9 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 app/overrides/spree/admin/orders/index/set_ng_app.deface diff --git a/app/assets/javascripts/admin/enterprise_fees.js b/app/assets/javascripts/admin/enterprise_fees.js index b815cd4266..79b1876263 100644 --- a/app/assets/javascripts/admin/enterprise_fees.js +++ b/app/assets/javascripts/admin/enterprise_fees.js @@ -34,7 +34,7 @@ angular.module('enterprise_fees', []) return function(scope, element, attrs) { if(scope.enterprise_fee.id) { var url = "/admin/enterprise_fees/" + scope.enterprise_fee.id - var html = ''; + var html = ''; //var html = 'Delete Delete'; element.append(html); } diff --git a/app/helpers/spree/admin/base_helper_decorator.rb b/app/helpers/spree/admin/base_helper_decorator.rb index 82bf794073..86e77431ba 100644 --- a/app/helpers/spree/admin/base_helper_decorator.rb +++ b/app/helpers/spree/admin/base_helper_decorator.rb @@ -5,7 +5,7 @@ module Spree def link_to_remove_fields(name, f, options = {}) name = '' if options[:no_text] options[:class] = '' unless options[:class] - options[:class] += 'no-text with-tip' if options[:no_text] + options[:class] += 'no-text' if options[:no_text] url = if f.object.persisted? options[:url] || [:admin, f.object] diff --git a/app/overrides/spree/admin/image_settings/edit/add_image_format.html.haml.deface b/app/overrides/spree/admin/image_settings/edit/add_image_format.html.haml.deface index deb79b5bbf..fe050c6c7c 100644 --- a/app/overrides/spree/admin/image_settings/edit/add_image_format.html.haml.deface +++ b/app/overrides/spree/admin/image_settings/edit/add_image_format.html.haml.deface @@ -3,7 +3,7 @@ - @styles.each_with_index do |(style_name, style_value), index| .field.three.columns = label_tag "attachment_styles[#{style_name}]", style_name - %a.destroy_style.with-tip{:alt => t(:destroy), :href => "#", :title => t(:destroy)} + %a.destroy_style{:alt => t(:destroy), :href => "#", :title => t(:destroy)} %i.icon-trash = text_field_tag "attachment_styles[#{style_name}][]", admin_image_settings_geometry_from_style(style_value), :class => 'fullwidth' %br/ diff --git a/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface b/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface index 75a2bc4689..1711343f3c 100644 --- a/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface +++ b/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface @@ -2,5 +2,5 @@ - if order.special_instructions.present? %br - %span{class: "icon-warning-sign with-tip", title: order.special_instructions} + %span{class: "icon-warning-sign", "ofn-with-tip" => order.special_instructions} notes diff --git a/app/overrides/spree/admin/orders/index/set_ng_app.deface b/app/overrides/spree/admin/orders/index/set_ng_app.deface new file mode 100644 index 0000000000..9ca071be11 --- /dev/null +++ b/app/overrides/spree/admin/orders/index/set_ng_app.deface @@ -0,0 +1,2 @@ +add_to_attributes "table#listing_orders" +attributes "ng-app" => "ofn.admin" diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml index 2594d5c9f3..d75a97bbb5 100644 --- a/app/views/admin/order_cycles/_row.html.haml +++ b/app/views/admin/order_cycles/_row.html.haml @@ -11,7 +11,7 @@ - suppliers = order_cycle.suppliers.merge(OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises) - supplier_list = suppliers.map(&:name).sort.join ', ' - if suppliers.count > 3 - %span.with-tip{'data-powertip' => supplier_list} + %span{'ofn-with-tip' => supplier_list} = suppliers.count suppliers - else @@ -21,7 +21,7 @@ - distributors = order_cycle.distributors.merge(OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises) - distributor_list = distributors.map(&:name).sort.join ', ' - if distributors.count > 3 - %span.with-tip{'data-powertip' => distributor_list} + %span{'ofn-with-tip' => distributor_list} = distributors.count distributors - else diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index e39fdde12a..8f062ff7db 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -11,7 +11,7 @@ %li = button_link_to "Show more", main_app.admin_order_cycles_path(params: { show_more: true }) -= form_for @order_cycle_set, :url => main_app.bulk_update_admin_order_cycles_path do |f| += form_for @order_cycle_set, url: main_app.bulk_update_admin_order_cycles_path, html: {"ng-app" => "admin.orderCycles"} do |f| %table.index#listing_order_cycles %colgroup %col diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 0ab782023d..46e5caf191 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -1,10 +1,13 @@ +- content_for :app_wrapper_attrs do + = "ng-app='admin.lineItems'" + - content_for :page_title do %h1.page-title Bulk Order Management - %a.with-tip{ 'data-powertip' => "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." } What's this? + %a{ 'ofn-with-tip' => "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." } What's this? = render :partial => 'spree/admin/shared/order_sub_menu' -%div{ ng: { app: 'admin.lineItems', controller: 'LineItemsCtrl' } } +%div{ ng: { controller: 'LineItemsCtrl' } } %save-bar{ save: "submit()", form: "bulk_order_form" } .filters{ :class => "sixteen columns alpha" } .date_filter{ :class => "two columns alpha" } 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 ff345cb259..fb68704b79 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 @@ -25,6 +25,6 @@ %td.actions %a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text", 'ng-show' => "variantSaved(variant)" } %td.actions - %span.icon-warning-sign.with-tip{ 'ng-if' => 'variant.variant_overrides', title: "This variant has {{variant.variant_overrides.length}} override(s)" } + %span.icon-warning-sign{ 'ng-if' => 'variant.variant_overrides', 'ofn-with-tip' => "This variant has {{variant.variant_overrides.length}} override(s)" } %td.actions %a{ 'ng-click' => 'deleteVariant(product,variant)', "ng-class" => '{disabled: product.variants.length < 2}', :class => "delete-variant icon-trash no-text" } From 9747b0cf96be6521479b5f30ad2786b69fa39498 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Feb 2016 09:18:46 +1100 Subject: [PATCH 17/54] Upgrade select2-rails --- Gemfile | 6 +++--- Gemfile.lock | 13 ++++++++----- app/assets/images/select2.png | Bin 0 -> 1144 bytes app/assets/stylesheets/admin/select2.css.scss | 8 ++++++++ 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100755 app/assets/images/select2.png create mode 100644 app/assets/stylesheets/admin/select2.css.scss diff --git a/Gemfile b/Gemfile index 8230c6a15f..300b17ffa3 100644 --- a/Gemfile +++ b/Gemfile @@ -8,9 +8,9 @@ gem 'i18n', '~> 0.6.11' gem 'nokogiri' gem 'pg' -gem 'spree', :github => 'openfoodfoundation/spree', :branch => '1-3-stable' -gem 'spree_i18n', :github => 'spree/spree_i18n', :branch => '1-3-stable' -gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '1-3-stable' +gem 'spree', github: 'openfoodfoundation/spree', branch: '1-3-stable' +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 # https://github.com/spree-contrib/better_spree_paypal_express/pull/117 diff --git a/Gemfile.lock b/Gemfile.lock index 37677c043c..75763fb4a0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,7 +23,7 @@ GIT GIT remote: git://github.com/openfoodfoundation/spree.git - revision: afcc23e489eb604a3e2651598a7c8364e2acc7b3 + revision: 6e3edfe40a5de8eba0095b2c5f3db9ea54c3afda branch: 1-3-stable specs: spree (1.3.6.beta) @@ -54,7 +54,7 @@ GIT rabl (= 0.7.2) rails (~> 3.2.16) ransack (= 0.7.2) - select2-rails (= 3.2.1) + select2-rails (= 3.5.9.3) state_machine (= 1.1.2) stringex (~> 1.3.2) truncate_html (~> 0.5.5) @@ -124,8 +124,8 @@ GEM active_link_to (1.0.0) active_model_serializers (0.8.3) activemodel (>= 3.0) - activemerchant (1.48.0) - activesupport (>= 3.2.14, < 5.0.0) + activemerchant (1.57.0) + activesupport (>= 3.2.14, < 5.1) builder (>= 2.1.2, < 4.0.0) i18n (>= 0.6.9) nokogiri (~> 1.4) @@ -574,7 +574,7 @@ GEM railties (~> 3.2.0) sass (>= 3.1.10) tilt (~> 1.3) - select2-rails (3.2.1) + select2-rails (3.5.9.3) thor (~> 0.14) shoulda-matchers (1.1.0) activesupport (>= 3.0.0) @@ -730,3 +730,6 @@ DEPENDENCIES whenever wicked_pdf wkhtmltopdf-binary + +BUNDLED WITH + 1.10.6 diff --git a/app/assets/images/select2.png b/app/assets/images/select2.png new file mode 100755 index 0000000000000000000000000000000000000000..9790e029f03d8b550fbf281dfac4b0d923849e70 GIT binary patch literal 1144 zcmV-;1c&>HP)|j?i^v|tli!Eq7}j+q=gU?8G@T*D~3u3wd9`T6d+)oRV_y8czYUg!3(nE|-n zZofiAgoxK0jm9ZSl0t}>CnC~y{Y%pUHyVv;MEt!2T}8wjo%_5j%TJmP7yv+$BrhWV zMjb)KYmy`d`pwo%PV0UE80i^X8DsaE(rGoD&C`fj?W!>%{@PV< zsZX}HwpIi|7`@$>Nu^ScA|etIWipxh|2x;%*M`^D*1qn->bkBblgUR8vstxT{ki9( zn9!cOtA`wwlwM9f!EEYdJ%tpCf?qa{D9Cocc1#H=G(#*_ExmYZI8VZG8uGMPq z9x74iILGSh>SxqrL_~ywAXsCun0BXtB9X|6dcA(xZnrO1DwV4M0D(Z@yrL-Qd7gh0 z0B#;AEXCvTlj(GN?x=?)pU-y@00;n#U@*843WZK{9M=K>;(7i%(`8!WaMMx7G;K~*)dzaUCskFQ zGi7QS2n3#GjO}*SnCJQLyXrkLF>&cYUV6yHN<=n?!;$rRy-#x-ry$~DqtQ4+L?fo` zcm#vNv*B>q-(%CBU@-VZKO-9i$QUzS8|&CM@$LcN12&lqWmpnL(Zv|!0RRlcsKjEi zgNK%33rj02EAPm%T6x9o z74Xn_X^X|;HVi}4G_BD8tle%ecQQn~X~NR(?(TJq#WLb>IG*Z%Ha0ePfr!vBjGAe@ zbRv;BMMNaaaxIg|EOc5>&Rj0{77>voNo%I_(%EeGrLI@r-rml)TCG2Oex4bHLg9m3 z0S{fZluD&$#>dBBb-7&A0Dv1C8{cpo=Zr?9uLA&-N~MpY(dcXYRZFI+hx7UTdp*7H zj|D+EG4v{ZfGq5iuInlhQ97MIYkFV+fJ&wE10ov9Wb&awz<&W$9*auLSrNwo0000< KMNUMnLSTZ>Z5%QH literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/admin/select2.css.scss b/app/assets/stylesheets/admin/select2.css.scss new file mode 100644 index 0000000000..e5d89c4ece --- /dev/null +++ b/app/assets/stylesheets/admin/select2.css.scss @@ -0,0 +1,8 @@ +.select2-container { + .select2-choice { + .select2-arrow { + background-image: none; + background-color: #5498da; + } + } +} From 1e288e5f140339333e9e70259abb6cae581059a1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 12 Feb 2016 11:10:56 +1100 Subject: [PATCH 18/54] Further styling for select2 after upgrade --- app/assets/images/select2x2.png | Bin 0 -> 1691 bytes app/assets/stylesheets/admin/select2.css.scss | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100755 app/assets/images/select2x2.png diff --git a/app/assets/images/select2x2.png b/app/assets/images/select2x2.png new file mode 100755 index 0000000000000000000000000000000000000000..7e737c98cfcbc252dac028ba93dd489599af7114 GIT binary patch literal 1691 zcmV;M24wk(P)1ApihydU~n=0L;$L%0agEdR+kk$mjEcHsHE0 z4vq$c>w03K{opbi?`Qw>;YHq2~0~Y|$gL(B}CQZ{` zWipx7emh;)UyGu6XBa>9V2)L<*A+jw005axW|bvgm}EsP$qFik!X{W)2XhKI2<}C) zXuPPLjUc$ab#NHSI0r{SjYgwXC=_G>fI^`lHyVx7R5UvLw70jnwYRso71h`3*JqyE zUUa{9cXw~I7rnc?dpoLt8Ngv#GPAj|B#Tmd;Et$vyL~Wez(+?%&)MHUIy!oe6+_55 z`0VUV9@Tb#e_x*DJXNh$H*&e$UG_Ma%iXP3s~b~4frs@N$8nww^Ipeso=w7Im@1Wu z=tYcAqDZAu5z&J~$S{ne>$>`YH;ir=MsbpF(rh+cOr=sQ{m-XTsg-OtTSN~I=`ogN zZ3TI+Wm#L3@|ga_Do8Bhn##t>MUxu8go{#oFv+B*Kd}lDOSmvZUoznelZ1^Uu?o^u zCtQ~)OL`M7mRL>FgZ&b#&snxzW#TAFk|Igcz&e=Oj3q<^0JPif+mup`QmRF@vA`o> z53CCSZV=>to3Q}C4y{4nMlZM^$U!!K=bH%|&V95s;CHByQdQOOxlKuuKvh*)k|ae{ z)sa_K;*uRcm+UA2@Cn3ZGWpsx%~hXpU3&NKos58^wI<6Kau~R{CW~*$08qLNT=ZbR zJ(%eg0IUXtYg|Dn0!ATV1dKw!C3R#De=k=bu=Z^QQXHUf@<-oAaikxr)%5{blPLdcqr$yy?jc$`kB8>gqI8|c9i zlD7imDIp}&`&DWHV8aE^r2!x#2*RJHY03yVBA%PgW`8Gy5D%aY01eBs9&|dLLZ{Qo z+qV4>0O|nHWyd8FiQn$uzkfBJjLT%kGY$_AMM|mW-T8o0N{w!}`$W^UWIqE<(27t8Zxn09Be!g+zMo|<+v)@(}MYYrEJa8PR1OSeg zq-DHwTrSzeB|GDhZQC#6@%WlIz1Ew}=K9LYiZSRIP1BO;bb22EmIxulahzH_9$&{f zm_yCX%&e35E9gkO-TrA9T+frulamw0dsDcvSnO*|viO&47{*_t8k?qBW|=i+(Gk&` zH*b>I$-2f(t+~0m??yEi1mRl%APAWM21kwkytugdOIq;@8<#IXM?RFo{&CM;muaXF9wOS%RIAmE z_4W0qtX1i{uG8ssnypsrkCT&=zX>6UR4Vn&ty{P534-tygUim*yH^9i1#eY_ZN>54 zlCV~*$vy2#N-1?HrH7d z*Sy)(j;v2?h>rNS zcEfg9!4MsxuzppLF+_75)`PZlS5>tpNzzE4G9h4&0S|&aYC97f9h@ulUWEuanvi>Y l+b>_fe2ASa6ngM9=s!}-KY1y12L%8C002ovPDHLkV1oPu6;J>G literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/admin/select2.css.scss b/app/assets/stylesheets/admin/select2.css.scss index e5d89c4ece..daba11d099 100644 --- a/app/assets/stylesheets/admin/select2.css.scss +++ b/app/assets/stylesheets/admin/select2.css.scss @@ -1,8 +1,10 @@ .select2-container { .select2-choice { .select2-arrow { + width: 22px; + border: none; background-image: none; - background-color: #5498da; + background-color: transparent; } } } From 977ff7b35d232a84246161eac2cfb190b3b1350b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 19 Feb 2016 15:34:15 +1100 Subject: [PATCH 19/54] Fix compatibility issues with new version of select2 --- .../admin/users/directives/user_select.js.coffee | 4 ++-- app/views/admin/variant_overrides/_filters.html.haml | 2 +- spec/features/admin/bulk_order_management_spec.rb | 8 ++++---- spec/support/matchers/select2_matchers.rb | 4 ++-- spec/support/request/web_helper.rb | 7 +++++++ 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/users/directives/user_select.js.coffee b/app/assets/javascripts/admin/users/directives/user_select.js.coffee index 94df1894d9..bde54fd6d0 100644 --- a/app/assets/javascripts/admin/users/directives/user_select.js.coffee +++ b/app/assets/javascripts/admin/users/directives/user_select.js.coffee @@ -2,12 +2,12 @@ angular.module("admin.users").directive "userSelect", -> scope: user: '&userSelect' model: '=ngModel' - link: (scope,element,attrs) -> + link: (scope, element, attrs) -> setTimeout -> element.select2 multiple: false initSelection: (element, callback) -> - callback {id: scope.user().id, email: scope.user().email} + callback {id: scope.user()?.id, email: scope.user()?.email} ajax: url: '/admin/search/known_users' datatype: 'json' diff --git a/app/views/admin/variant_overrides/_filters.html.haml b/app/views/admin/variant_overrides/_filters.html.haml index 9dc90d7ad3..d5f8bb9714 100644 --- a/app/views/admin/variant_overrides/_filters.html.haml +++ b/app/views/admin/variant_overrides/_filters.html.haml @@ -11,7 +11,7 @@ .filter_select.four.columns %label{ :for => 'producer_filter', ng: {class: '{disabled: !hub.id}'} }Producer %br - %input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', style: 'display:none', data: 'producers', blank: "{id: 0, name: 'All'}", ng: { model: 'producerFilter', disabled: '!hub.id' } } + %input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', data: 'producers', blank: "{id: 0, name: 'All'}", ng: { model: 'producerFilter', disabled: '!hub.id' } } -# .filter_select{ :class => "three columns" } -# %label{ :for => 'distributor_filter' }Hub -# %br diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 1e011b5939..d28a20d6f9 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -236,9 +236,9 @@ feature %q{ 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 + open_select2 "div.select2-container#s2id_supplier_filter" supplier_names.each { |sn| expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: sn } - find("div.select2-container#s2id_supplier_filter").click + close_select2 "div.select2-container#s2id_supplier_filter" expect(page).to have_selector "tr#li_#{li1.id}", visible: true expect(page).to have_selector "tr#li_#{li2.id}", visible: true select2_select s1.name, from: "supplier_filter" @@ -271,9 +271,9 @@ feature %q{ 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 + open_select2 "div.select2-container#s2id_distributor_filter" distributor_names.each { |dn| expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: dn } - find("div.select2-container#s2id_distributor_filter").click + close_select2 "div.select2-container#s2id_distributor_filter" expect(page).to have_selector "tr#li_#{li1.id}", visible: true expect(page).to have_selector "tr#li_#{li2.id}", visible: true select2_select d1.name, from: "distributor_filter" diff --git a/spec/support/matchers/select2_matchers.rb b/spec/support/matchers/select2_matchers.rb index e1f821753d..6ab4fd5788 100644 --- a/spec/support/matchers/select2_matchers.rb +++ b/spec/support/matchers/select2_matchers.rb @@ -103,9 +103,9 @@ RSpec::Matchers.define :have_select2 do |id, options={}| end def with_select2_open(from) - find(from).click + open_select2 from r = yield - find(from).click + close_select2 from r end end diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index 1dd5adc7a3..30ce677c6c 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -144,6 +144,13 @@ module WebHelper have_selector "div.select2-result-label", text: value end + def open_select2(selector) + page.evaluate_script "jQuery('#{selector}').select2('open');" + end + + def close_select2(selector) + page.evaluate_script "jQuery('#{selector}').select2('close');" + end private def wait_for_ajax From ae03170984d2628557bcf752f04d51de1543726c Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 19 Feb 2016 16:06:19 +1100 Subject: [PATCH 20/54] Sanitize input for ofn-select2 --- .../admin/index_utils/directives/ofn-select2.js.coffee | 4 +++- .../javascripts/admin/index_utils/index_utils.js.coffee | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee b/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee index ba7a4b54df..ec454e9216 100644 --- a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.indexUtils").directive "ofnSelect2", ($timeout, blankOption) -> +angular.module("admin.indexUtils").directive "ofnSelect2", ($sanitize, $timeout) -> require: 'ngModel' restrict: 'C' scope: @@ -10,6 +10,8 @@ angular.module("admin.indexUtils").directive "ofnSelect2", ($timeout, blankOptio $timeout -> scope.text ||= 'name' scope.data.unshift(scope.blank) if scope.blank? && typeof scope.blank is "object" + + item.name = $sanitize(item.name) for item in scope.data element.select2 minimumResultsForSearch: scope.minSearch || 0 data: { results: scope.data, text: scope.text } diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee index adcd68e3c5..5e5b5cadf2 100644 --- a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -1 +1 @@ -angular.module("admin.indexUtils", ['ngResource', 'templates']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file +angular.module("admin.indexUtils", ['ngResource', 'ngSanitize', 'templates']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file From 4314bfb99c1f43a8792df40eba12cc3dacae2480 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 19 Feb 2016 16:46:13 +1100 Subject: [PATCH 21/54] Fix karma fail due to file load order change --- .../admin/{ofn_admin.js.coffee => admin_ofn.js.coffee} | 0 app/assets/javascripts/admin/all.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/assets/javascripts/admin/{ofn_admin.js.coffee => admin_ofn.js.coffee} (100%) diff --git a/app/assets/javascripts/admin/ofn_admin.js.coffee b/app/assets/javascripts/admin/admin_ofn.js.coffee similarity index 100% rename from app/assets/javascripts/admin/ofn_admin.js.coffee rename to app/assets/javascripts/admin/admin_ofn.js.coffee diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index b4bd1ca5ec..60ca6f330a 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -21,7 +21,7 @@ //= require ../shared/ng-tags-input.min.js //= require angular-rails-templates //= require_tree ../templates/admin -//= require ./ofn_admin +//= require ./admin_ofn //= require ./accounts_and_billing_settings/accounts_and_billing_settings //= require ./business_model_configuration/business_model_configuration //= require ./customers/customers From 6193bb896b21537930a72f1f6df93d9d451c19e7 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 24 Feb 2016 11:08:51 +1100 Subject: [PATCH 22/54] Sanitize input for ofnTaxonAutocomplete and userSelect directives --- .../admin/taxons/directives/taxon_autocomplete.js.coffee | 4 ++-- app/assets/javascripts/admin/taxons/taxons.js.coffee | 2 +- .../javascripts/admin/users/directives/user_select.js.coffee | 5 +++-- 3 files changed, 6 insertions(+), 5 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 b978a050ad..b1eac64569 100644 --- a/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.taxons").directive "ofnTaxonAutocomplete", (Taxons) -> +angular.module("admin.taxons").directive "ofnTaxonAutocomplete", (Taxons, $sanitize) -> # Adapted from Spree's existing taxon autocompletion scope: true link: (scope,element,attrs) -> @@ -18,7 +18,7 @@ angular.module("admin.taxons").directive "ofnTaxonAutocomplete", (Taxons) -> query: (query) -> query.callback { results: Taxons.findByTerm(query.term) } formatResult: (taxon) -> - taxon.name + $sanitize(taxon.name) formatSelection: (taxon) -> taxon.name diff --git a/app/assets/javascripts/admin/taxons/taxons.js.coffee b/app/assets/javascripts/admin/taxons/taxons.js.coffee index 863e6e8125..07de167ccf 100644 --- a/app/assets/javascripts/admin/taxons/taxons.js.coffee +++ b/app/assets/javascripts/admin/taxons/taxons.js.coffee @@ -1 +1 @@ -angular.module("admin.taxons", []) \ No newline at end of file +angular.module("admin.taxons", ['ngSanitize']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/users/directives/user_select.js.coffee b/app/assets/javascripts/admin/users/directives/user_select.js.coffee index bde54fd6d0..787ef2124b 100644 --- a/app/assets/javascripts/admin/users/directives/user_select.js.coffee +++ b/app/assets/javascripts/admin/users/directives/user_select.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.users").directive "userSelect", -> +angular.module("admin.users").directive "userSelect", ($sanitize) -> scope: user: '&userSelect' model: '=ngModel' @@ -11,9 +11,10 @@ angular.module("admin.users").directive "userSelect", -> ajax: url: '/admin/search/known_users' datatype: 'json' - data:(term, page) -> + data: (term, page) -> { q: term } results: (data, page) -> + item.email = $sanitize(item.email) for item in data { results: data } formatResult: (user) -> user.email From 1770cbb6bf6b0d326fa96bc055cbcf61749a2d19 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 26 Feb 2016 16:09:14 +1100 Subject: [PATCH 23/54] Display footer_email in confirmation emails correctly A bug introduced in a9c37c162e1956028704fbdf74ce1c56c5b3ce7d caused the creation of confirmation emails for shops to fail. The email template got fixed now and the email address from the database is displayed if present. --- app/views/shared/mailers/_social_and_contact.html.haml | 3 +-- spec/mailers/order_mailer_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/views/shared/mailers/_social_and_contact.html.haml b/app/views/shared/mailers/_social_and_contact.html.haml index 4f5222f77d..d76925c641 100644 --- a/app/views/shared/mailers/_social_and_contact.html.haml +++ b/app/views/shared/mailers/_social_and_contact.html.haml @@ -24,6 +24,5 @@ %h5 = t :email_contact %strong - %a{href: ContentConfig.footer_email.reverse, mailto: true, target: '_blank'} - #{ContentConfig.footer_email} + = mail_to ContentConfig.footer_email %span.clear diff --git a/spec/mailers/order_mailer_spec.rb b/spec/mailers/order_mailer_spec.rb index a472e95979..6e88742fe7 100644 --- a/spec/mailers/order_mailer_spec.rb +++ b/spec/mailers/order_mailer_spec.rb @@ -40,5 +40,12 @@ describe Spree::OrderMailer do ActionMailer::Base.deliveries.count.should == 1 ActionMailer::Base.deliveries.first.to.should == [@distributor.email] end + + it "sends an email even if a footer_email is given" do + # Testing bug introduced by a9c37c162e1956028704fbdf74ce1c56c5b3ce7d + ContentConfig.footer_email = "email@example.com" + Spree::OrderMailer.confirm_email_for_shop(@order1.id).deliver + ActionMailer::Base.deliveries.count.should == 1 + end end end From 137003c671278c1d2495dfe47228cf04babe01de Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 22 Jan 2016 15:31:59 +1100 Subject: [PATCH 24/54] Correct calculation of tax on EnterpriseFees with TaxRates where included_in_price=false --- .../spree/calculator/default_tax_decorator.rb | 39 +++++++++++++++++++ .../enterprise_fee_applicator.rb | 2 +- spec/models/spree/adjustment_spec.rb | 35 +++++++++++++---- 3 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 app/models/spree/calculator/default_tax_decorator.rb diff --git a/app/models/spree/calculator/default_tax_decorator.rb b/app/models/spree/calculator/default_tax_decorator.rb new file mode 100644 index 0000000000..e55cfc5536 --- /dev/null +++ b/app/models/spree/calculator/default_tax_decorator.rb @@ -0,0 +1,39 @@ +require 'open_food_network/enterprise_fee_calculator' + +Spree::Calculator::DefaultTax.class_eval do + + private + + # Override this method to enable calculation of tax for + # enterprise fees with tax rates where included_in_price = false + def compute_order(order) + matched_line_items = order.line_items.select do |line_item| + line_item.product.tax_category == rate.tax_category + end + + line_items_total = matched_line_items.sum(&:total) + + # Added this line + calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(order.distributor, order.order_cycle) + + # Added this block, finds relevant fees for each line_item, calculates the tax on them, and returns the total tax + per_item_fees_total = order.line_items.sum do |line_item| + calculator.send(:per_item_enterprise_fee_applicators_for, line_item.variant) + .select { |applicator| applicator.enterprise_fee.tax_category == rate.tax_category } + .sum { |applicator| applicator.enterprise_fee.compute_amount(line_item) } + end + + # Added this block, finds relevant fees for whole order, calculates the tax on them, and returns the total tax + per_order_fees_total = calculator.send(:per_order_enterprise_fee_applicators_for, order) + .select { |applicator| applicator.enterprise_fee.tax_category == rate.tax_category } + .sum { |applicator| applicator.enterprise_fee.compute_amount(order) } + + # round_to_two_places(line_items_total * rate.amount) # Removed this line + + # Added this block + [line_items_total, per_item_fees_total, per_order_fees_total].sum do |total| + round_to_two_places(total * rate.amount) + end + end + +end diff --git a/lib/open_food_network/enterprise_fee_applicator.rb b/lib/open_food_network/enterprise_fee_applicator.rb index 4962bc148e..26b1397193 100644 --- a/lib/open_food_network/enterprise_fee_applicator.rb +++ b/lib/open_food_network/enterprise_fee_applicator.rb @@ -34,7 +34,7 @@ module OpenFoodNetwork def adjustment_tax(order, adjustment) tax_rates = enterprise_fee.tax_category ? enterprise_fee.tax_category.tax_rates.match(order) : [] - tax_rates.sum do |rate| + tax_rates.select(&:included_in_price).sum do |rate| rate.compute_tax adjustment.amount end end diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index 512e720f54..cd49e3893a 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -137,21 +137,25 @@ module Spree context "when enterprise fees are taxed per-order" do let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: tax_category, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) } - it "records the tax on the enterprise fee adjustments" do - # The fee is $50, tax is 10%, and the fee is inclusive of tax - # Therefore, the included tax should be 0.1/1.1 * 50 = $4.55 + describe "when the tax rate includes the tax in the price" do + it "records the tax on the enterprise fee adjustments" do + # The fee is $50, tax is 10%, and the fee is inclusive of tax + # Therefore, the included tax should be 0.1/1.1 * 50 = $4.55 - adjustment.included_tax.should == 4.55 + adjustment.included_tax.should == 4.55 + end end describe "when the tax rate does not include the tax in the price" do before do tax_rate.update_attribute :included_in_price, false + order.create_tax_charge! # Updating line_item or order has the same effect order.update_distribution_charge! end - it "treats it as inclusive anyway" do - adjustment.included_tax.should == 4.55 + it "records the tax on TaxRate adjustment on the order" do + adjustment.included_tax.should == 0 + order.adjustments.tax.first.amount.should == 5.0 end end @@ -172,8 +176,23 @@ module Spree context "when enterprise fees are taxed per-item" do let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: tax_category, calculator: Calculator::PerItem.new(preferred_amount: 50.0)) } - it "records the tax on the enterprise fee adjustments" do - adjustment.included_tax.should == 4.55 + describe "when the tax rate includes the tax in the price" do + it "records the tax on the enterprise fee adjustments" do + adjustment.included_tax.should == 4.55 + end + end + + describe "when the tax rate does not include the tax in the price" do + before do + tax_rate.update_attribute :included_in_price, false + order.create_tax_charge! # Updating line_item or order has the same effect + order.update_distribution_charge! + end + + it "records the tax on TaxRate adjustment on the order" do + adjustment.included_tax.should == 0 + order.adjustments.tax.first.amount.should == 5.0 + end end end end From 69ee1a98a72eb297b128bcafb90465e4774cdf4c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 29 Jan 2016 12:41:57 +1100 Subject: [PATCH 25/54] EnterpriseFeeCalculator return empty list when retrieving applicators without distributor or order_cycle being present --- .../enterprise_fee_calculator.rb | 4 + .../enterprise_fee_calculator_spec.rb | 137 ++++++++++-------- 2 files changed, 79 insertions(+), 62 deletions(-) diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb index d65a80c0dc..7222abad1c 100644 --- a/lib/open_food_network/enterprise_fee_calculator.rb +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -112,6 +112,8 @@ module OpenFoodNetwork def per_item_enterprise_fee_applicators_for(variant) fees = [] + return [] unless @order_cycle && @distributor + @order_cycle.exchanges_carrying(variant, @distributor).each do |exchange| exchange.enterprise_fees.per_item.each do |enterprise_fee| fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) @@ -128,6 +130,8 @@ module OpenFoodNetwork def per_order_enterprise_fee_applicators_for(order) fees = [] + return fees unless @order_cycle && order.distributor + @order_cycle.exchanges_supplying(order).each do |exchange| exchange.enterprise_fees.per_order.each do |enterprise_fee| fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb index d2d673e5ab..a31295ac9d 100644 --- a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -194,77 +194,90 @@ module OpenFoodNetwork end end - describe "creating adjustments for a line item" do - let(:oc) { OrderCycle.new } - let(:variant) { double(:variant) } - let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor, order_cycle: oc) } - let(:line_item) { double(:line_item, variant: variant, order: order) } - - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_line_item_adjustment).with(line_item) - - efc = EnterpriseFeeCalculator.new - efc.should_receive(:per_item_enterprise_fee_applicators_for).with(variant) { [applicator] } - - efc.create_line_item_adjustments_for line_item - end - - it "makes fee applicators for a line item" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_item) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_item) { [ef2] } - - oc.stub(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_item) { [ef3] } - - efc = EnterpriseFeeCalculator.new(distributor, oc) - efc.send(:per_item_enterprise_fee_applicators_for, line_item.variant).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] - end - end - - describe "creating adjustments for an order" do + describe "creating adjustments" do let(:oc) { OrderCycle.new } let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + let(:ef1) { double(:enterprise_fee) } + let(:ef2) { double(:enterprise_fee) } + let(:ef3) { double(:enterprise_fee) } + let(:incoming_exchange) { double(:exchange, role: 'supplier') } + let(:outgoing_exchange) { double(:exchange, role: 'distributor') } + let(:applicator) { double(:enterprise_fee_applicator) } - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_order_adjustment).with(order) - efc = EnterpriseFeeCalculator.new - efc.should_receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } + describe "for a line item" do + let(:variant) { double(:variant) } + let(:line_item) { double(:line_item, variant: variant, order: order) } - efc.create_order_adjustments_for order + before do + allow(incoming_exchange).to receive(:enterprise_fees) { double(:enterprise_fees, per_item: [ef1]) } + allow(outgoing_exchange).to receive(:enterprise_fees) { double(:enterprise_fees, per_item: [ef2]) } + allow(oc).to receive(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } + allow(oc).to receive(:coordinator_fees) { double(:coodinator_fees, per_item: [ef3]) } + end + + context "with order_cycle and distributor set" do + let(:efc) { EnterpriseFeeCalculator.new(distributor, oc) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + + it "creates an adjustment for each fee" do + expect(efc).to receive(:per_item_enterprise_fee_applicators_for).with(variant) { [applicator] } + expect(applicator).to receive(:create_line_item_adjustment).with(line_item) + efc.create_line_item_adjustments_for line_item + end + + it "makes fee applicators for a line item" do + expect(efc.send(:per_item_enterprise_fee_applicators_for, line_item.variant)) + .to eq [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] + end + end + + context "with no order_cycle or distributor set" do + let(:efc) { EnterpriseFeeCalculator.new } + let(:order) { double(:order, distributor: nil, order_cycle: nil) } + + it "does not make applicators for an order" do + expect(efc.send(:per_item_enterprise_fee_applicators_for, line_item.variant)).to eq [] + end + end end - it "makes fee applicators for an order" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_order) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_order) { [ef2] } + describe "for an order" do + before do + allow(incoming_exchange).to receive(:enterprise_fees) { double(:enterprise_fees, per_order: [ef1]) } + allow(outgoing_exchange).to receive(:enterprise_fees) { double(:enterprise_fees, per_order: [ef2]) } + allow(oc).to receive(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } + allow(oc).to receive(:coordinator_fees) { double(:coodinator_fees, per_order: [ef3]) } + end - oc.stub(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_order) { [ef3] } + context "with order_cycle and distributor set" do + let(:efc) { EnterpriseFeeCalculator.new(distributor, oc) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } - efc = EnterpriseFeeCalculator.new(distributor, oc) - efc.send(:per_order_enterprise_fee_applicators_for, order).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] + it "creates an adjustment for each fee" do + expect(efc).to receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } + expect(applicator).to receive(:create_order_adjustment).with(order) + efc.create_order_adjustments_for order + end + + it "makes fee applicators for an order" do + expect(efc.send(:per_order_enterprise_fee_applicators_for, order)) + .to eq [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] + end + end + + context "with no order_cycle or distributor set" do + let(:efc) { EnterpriseFeeCalculator.new } + let(:order) { double(:order, distributor: nil, order_cycle: nil) } + + it "does not make applicators for an order" do + expect(efc.send(:per_order_enterprise_fee_applicators_for, order)).to eq [] + end + end end end end From 0bd9dc7af0759010c0fc5594a5a5ac0e7b873fe9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 3 Feb 2016 15:10:52 +1100 Subject: [PATCH 26/54] Enterprise Fees: Splitting ng controllers and directives into separate files, reanming module and coffee-ising --- app/assets/javascripts/admin/all.js | 1 + .../javascripts/admin/enterprise_fees.js | 69 ------------------- .../enterprise_fees_controller.js.coffee | 17 +++++ .../bind_html_unsafe_compiled.js.coffee | 6 ++ .../directives/delete_resource.js.coffee | 8 +++ ...alculator_preferences_match_type.js.coffee | 21 ++++++ .../admin/enterprise_fees/enterprise_fees.js | 1 + .../admin/enterprise_fees/index.html.haml | 2 +- 8 files changed, 55 insertions(+), 70 deletions(-) delete mode 100644 app/assets/javascripts/admin/enterprise_fees.js create mode 100644 app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee create mode 100644 app/assets/javascripts/admin/enterprise_fees/directives/bind_html_unsafe_compiled.js.coffee create mode 100644 app/assets/javascripts/admin/enterprise_fees/directives/delete_resource.js.coffee create mode 100644 app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee create mode 100644 app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 60ca6f330a..3485f45e23 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -27,6 +27,7 @@ //= require ./customers/customers //= require ./dropdown/dropdown //= require ./enterprises/enterprises +//= require ./enterprise_fees/enterprise_fees //= require ./enterprise_groups/enterprise_groups //= require ./index_utils/index_utils //= require ./line_items/line_items diff --git a/app/assets/javascripts/admin/enterprise_fees.js b/app/assets/javascripts/admin/enterprise_fees.js deleted file mode 100644 index 79b1876263..0000000000 --- a/app/assets/javascripts/admin/enterprise_fees.js +++ /dev/null @@ -1,69 +0,0 @@ -angular.module('enterprise_fees', []) - .controller('AdminEnterpriseFeesCtrl', ['$scope', '$http', '$window', function($scope, $http, $window) { - $scope.enterpriseFeesUrl = function() { - var url = '/admin/enterprise_fees.json?include_calculators=1'; - - var match = $window.location.search.match(/enterprise_id=(\d+)/); - if(match) { - url += "&"+match[0]; - } - - return url; - }; - - $http.get($scope.enterpriseFeesUrl()).success(function(data) { - $scope.enterprise_fees = data; - - // TODO: Angular 1.1.0 will have a means to reset a form to its pristine state, which - // would avoid the need to save off original calculator types for comparison. - for(i in $scope.enterprise_fees) { - $scope.enterprise_fees[i].orig_calculator_type = $scope.enterprise_fees[i].calculator_type; - } - }); - }]) - - .directive('ngBindHtmlUnsafeCompiled', ['$compile', function($compile) { - return function(scope, element, attrs) { - scope.$watch(attrs.ngBindHtmlUnsafeCompiled, function(value) { - element.html($compile(value)(scope)); - }); - } - }]) - - .directive('spreeDeleteResource', function() { - return function(scope, element, attrs) { - if(scope.enterprise_fee.id) { - var url = "/admin/enterprise_fees/" + scope.enterprise_fee.id - var html = ''; - //var html = 'Delete Delete'; - element.append(html); - } - } - }) - - .directive('spreeEnsureCalculatorPreferencesMatchType', function() { - // Hide calculator preference fields when calculator type changed - // Fixes 'Enterprise fee is not found' error when changing calculator type - // See spree/core/app/assets/javascripts/admin/calculator.js - - // Note: For some reason, DOM --> model bindings aren't working here, so - // we use element.val() instead of querying the model itself. - - return function(scope, element, attrs) { - scope.$watch(function(scope) { - //return scope.enterprise_fee.calculator_type; - return element.val(); - }, function(value) { - var settings = element.parent().parent().find("div.calculator-settings"); - - // scope.enterprise_fee.calculator_type == scope.enterprise_fee.orig_calculator_type - if(element.val() == scope.enterprise_fee.orig_calculator_type) { - settings.show(); - settings.find("input").prop("disabled", false); - } else { - settings.hide(); - settings.find("input").prop("disabled", true); - } - }); - } - }); diff --git a/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee b/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee new file mode 100644 index 0000000000..3dd860fb56 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee @@ -0,0 +1,17 @@ +angular.module('admin.enterpriseFees').controller 'enterpriseFeesCtrl', ($scope, $http, $window) -> + $scope.enterpriseFeesUrl = -> + url = '/admin/enterprise_fees.json?include_calculators=1' + match = $window.location.search.match(/enterprise_id=(\d+)/) + if match + url += '&' + match[0] + url + + $http.get($scope.enterpriseFeesUrl()).success (data) -> + $scope.enterprise_fees = data + # TODO: Angular 1.1.0 will have a means to reset a form to its pristine state, which + # would avoid the need to save off original calculator types for comparison. + for i of $scope.enterprise_fees + $scope.enterprise_fees[i].orig_calculator_type = $scope.enterprise_fees[i].calculator_type + return + + return diff --git a/app/assets/javascripts/admin/enterprise_fees/directives/bind_html_unsafe_compiled.js.coffee b/app/assets/javascripts/admin/enterprise_fees/directives/bind_html_unsafe_compiled.js.coffee new file mode 100644 index 0000000000..96c2292257 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/directives/bind_html_unsafe_compiled.js.coffee @@ -0,0 +1,6 @@ +angular.module("admin.enterpriseFees").directive 'ngBindHtmlUnsafeCompiled', ($compile) -> + (scope, element, attrs) -> + scope.$watch attrs.ngBindHtmlUnsafeCompiled, (value) -> + element.html $compile(value)(scope) + return + return diff --git a/app/assets/javascripts/admin/enterprise_fees/directives/delete_resource.js.coffee b/app/assets/javascripts/admin/enterprise_fees/directives/delete_resource.js.coffee new file mode 100644 index 0000000000..0ae1b3f6fd --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/directives/delete_resource.js.coffee @@ -0,0 +1,8 @@ +angular.module('admin.enterpriseFees').directive 'spreeDeleteResource', -> + (scope, element, attrs) -> + if scope.enterprise_fee.id + url = '/admin/enterprise_fees/' + scope.enterprise_fee.id + html = '' + #var html = 'Delete Delete'; + element.append html + return diff --git a/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee b/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee new file mode 100644 index 0000000000..5b128b4d34 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee @@ -0,0 +1,21 @@ +angular.module("admin.enterpriseFees").directive 'spreeEnsureCalculatorPreferencesMatchType', -> + # Hide calculator preference fields when calculator type changed + # Fixes 'Enterprise fee is not found' error when changing calculator type + # See spree/core/app/assets/javascripts/admin/calculator.js + # Note: For some reason, DOM --> model bindings aren't working here, so + # we use element.val() instead of querying the model itself. + (scope, element, attrs) -> + scope.$watch ((scope) -> + #return scope.enterprise_fee.calculator_type; + element.val() + ), (value) -> + settings = element.parent().parent().find('div.calculator-settings') + # scope.enterprise_fee.calculator_type == scope.enterprise_fee.orig_calculator_type + if element.val() == scope.enterprise_fee.orig_calculator_type + settings.show() + settings.find('input').prop 'disabled', false + else + settings.hide() + settings.find('input').prop 'disabled', true + return + return diff --git a/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js b/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js new file mode 100644 index 0000000000..a256834946 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js @@ -0,0 +1 @@ +angular.module("admin.enterpriseFees", []) diff --git a/app/views/admin/enterprise_fees/index.html.haml b/app/views/admin/enterprise_fees/index.html.haml index 08199d4c4a..60d818ed34 100644 --- a/app/views/admin/enterprise_fees/index.html.haml +++ b/app/views/admin/enterprise_fees/index.html.haml @@ -1,7 +1,7 @@ = content_for :page_title do Enterprise Fees -= 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| += ng_form_for @enterprise_fee_set, :url => main_app.bulk_update_admin_enterprise_fees_path, :html => {'ng-app' => 'admin.enterpriseFees', 'ng-controller' => 'enterpriseFeesCtrl'} do |enterprise_fee_set_form| = hidden_field_tag 'enterprise_id', @enterprise.id if @enterprise = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise_fee_set } From a66582a8fbc088f7aeaf6a8b58937f18e668bb98 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 4 Feb 2016 10:42:26 +1100 Subject: [PATCH 27/54] WIP: Using directive for EnterpriseFee index select elements, to properly bind data to the model --- .../enterprise_fees_controller.js.coffee | 13 +++++-------- ...calculator_preferences_match_type.js.coffee | 12 ++++-------- .../admin/enterprise_fees/enterprise_fees.js | 2 +- .../directives/ofn-select.js.coffee | 13 +++++++++++++ app/helpers/admin/injection_helper.rb | 4 ++-- app/helpers/angular_form_builder.rb | 12 ++++++++---- app/helpers/angular_form_helper.rb | 3 +-- app/helpers/enterprise_fees_helper.rb | 8 ++++++++ .../api/admin/calculator_serializer.rb | 11 +++++++++++ .../admin/enterprise_fees/_data.html.haml | 3 +++ .../admin/enterprise_fees/index.html.haml | 18 ++++++++++++++---- spec/features/admin/enterprise_fees_spec.rb | 12 ++++++------ 12 files changed, 76 insertions(+), 35 deletions(-) create mode 100644 app/assets/javascripts/admin/index_utils/directives/ofn-select.js.coffee create mode 100644 app/serializers/api/admin/calculator_serializer.rb create mode 100644 app/views/admin/enterprise_fees/_data.html.haml diff --git a/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee b/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee index 3dd860fb56..fee2140dcc 100644 --- a/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee @@ -1,4 +1,8 @@ -angular.module('admin.enterpriseFees').controller 'enterpriseFeesCtrl', ($scope, $http, $window) -> +angular.module('admin.enterpriseFees').controller 'enterpriseFeesCtrl', ($scope, $http, $window, enterprises, tax_categories, calculators) -> + $scope.enterprises = enterprises + $scope.tax_categories = tax_categories + $scope.calculators = calculators + $scope.enterpriseFeesUrl = -> url = '/admin/enterprise_fees.json?include_calculators=1' match = $window.location.search.match(/enterprise_id=(\d+)/) @@ -8,10 +12,3 @@ angular.module('admin.enterpriseFees').controller 'enterpriseFeesCtrl', ($scope, $http.get($scope.enterpriseFeesUrl()).success (data) -> $scope.enterprise_fees = data - # TODO: Angular 1.1.0 will have a means to reset a form to its pristine state, which - # would avoid the need to save off original calculator types for comparison. - for i of $scope.enterprise_fees - $scope.enterprise_fees[i].orig_calculator_type = $scope.enterprise_fees[i].calculator_type - return - - return diff --git a/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee b/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee index 5b128b4d34..4376ce59ef 100644 --- a/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee +++ b/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee @@ -2,16 +2,12 @@ angular.module("admin.enterpriseFees").directive 'spreeEnsureCalculatorPreferenc # Hide calculator preference fields when calculator type changed # Fixes 'Enterprise fee is not found' error when changing calculator type # See spree/core/app/assets/javascripts/admin/calculator.js - # Note: For some reason, DOM --> model bindings aren't working here, so - # we use element.val() instead of querying the model itself. (scope, element, attrs) -> - scope.$watch ((scope) -> - #return scope.enterprise_fee.calculator_type; - element.val() - ), (value) -> + orig_calculator_type = scope.enterprise_fee.calculator_type + + scope.$watch "enterprise_fee.calculator_type", (value) -> settings = element.parent().parent().find('div.calculator-settings') - # scope.enterprise_fee.calculator_type == scope.enterprise_fee.orig_calculator_type - if element.val() == scope.enterprise_fee.orig_calculator_type + if value == orig_calculator_type settings.show() settings.find('input').prop 'disabled', false else diff --git a/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js b/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js index a256834946..9c4e2ed4a5 100644 --- a/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js +++ b/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js @@ -1 +1 @@ -angular.module("admin.enterpriseFees", []) +angular.module("admin.enterpriseFees", ['admin.indexUtils']) diff --git a/app/assets/javascripts/admin/index_utils/directives/ofn-select.js.coffee b/app/assets/javascripts/admin/index_utils/directives/ofn-select.js.coffee new file mode 100644 index 0000000000..9a653d24f8 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/ofn-select.js.coffee @@ -0,0 +1,13 @@ +# Mainly useful for adding a blank option that works with AngularJS +# Angular doesn't seem to understand the blank option generated by rails +# using the include_blank flag on select helper. +angular.module("admin.indexUtils").directive "ofnSelect", -> + restrict: 'E' + scope: + data: "=" + replace: true + template: (element, attrs) -> + valueAttr = attrs.valueAttr || 'id' + textAttr = attrs.textAttr || 'name' + blank = if attrs.includeBlank? then "" else "" + return "" diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 343e15ef29..ba65e37860 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -56,8 +56,8 @@ module Admin admin_inject_json_ams_array "ofn.admin", "products", @products, Api::Admin::ProductSerializer end - def admin_inject_tax_categories - admin_inject_json_ams_array "ofn.admin", "tax_categories", @tax_categories, Api::Admin::TaxCategorySerializer + def admin_inject_tax_categories(opts={module: 'ofn.admin'}) + admin_inject_json_ams_array opts[:module], "tax_categories", @tax_categories, Api::Admin::TaxCategorySerializer end def admin_inject_taxons diff --git a/app/helpers/angular_form_builder.rb b/app/helpers/angular_form_builder.rb index 56fcab79fa..4dff1f92e1 100644 --- a/app/helpers/angular_form_builder.rb +++ b/app/helpers/angular_form_builder.rb @@ -11,26 +11,26 @@ class AngularFormBuilder < ActionView::Helpers::FormBuilder # @fields_for_record_name --> :collection # @object.send(@fields_for_record_name).first.class.to_s.underscore --> enterprise_fee - value = "{{ #{@object.send(@fields_for_record_name).first.class.to_s.underscore}.#{method} }}" + value = "{{ #{angular_model(method)} }}" options.reverse_merge!({'id' => angular_id(method)}) @template.text_field_tag angular_name(method), value, options end def ng_hidden_field(method, options = {}) - value = "{{ #{@object.send(@fields_for_record_name).first.class.to_s.underscore}.#{method} }}" + value = "{{ #{angular_model(method)} }}" @template.hidden_field_tag angular_name(method), value, :id => angular_id(method) end def ng_select(method, choices, angular_field, options = {}) - options.reverse_merge!({'id' => angular_id(method)}) + options.reverse_merge!({'id' => angular_id(method), 'ng-model' => "#{angular_model(method)}"}) @template.select_tag angular_name(method), @template.ng_options_for_select(choices, angular_field), options end def ng_collection_select(method, collection, value_method, text_method, angular_field, options = {}) - options.reverse_merge!({'id' => angular_id(method)}) + options.reverse_merge!({'id' => angular_id(method), 'ng-model' => "#{angular_model(method)}"}) @template.select_tag angular_name(method), @template.ng_options_from_collection_for_select(collection, value_method, text_method, angular_field), options end @@ -43,4 +43,8 @@ class AngularFormBuilder < ActionView::Helpers::FormBuilder def angular_id(method) "#{@object_name}_#{@fields_for_record_name}_attributes_{{ $index }}_#{method}" end + + def angular_model(method) + "#{@object.send(@fields_for_record_name).first.class.to_s.underscore}.#{method}" + end end diff --git a/app/helpers/angular_form_helper.rb b/app/helpers/angular_form_helper.rb index a7fd6e0e28..0687f62188 100644 --- a/app/helpers/angular_form_helper.rb +++ b/app/helpers/angular_form_helper.rb @@ -5,8 +5,7 @@ module AngularFormHelper container.map do |element| html_attributes = option_html_attributes(element) text, value = option_text_and_value(element).map { |item| item.to_s } - selected_attribute = %Q( ng-selected="#{angular_field} == '#{value}'") if angular_field - %() + %() end.join("\n").html_safe end diff --git a/app/helpers/enterprise_fees_helper.rb b/app/helpers/enterprise_fees_helper.rb index b7ec2b9018..82d24ce1a6 100644 --- a/app/helpers/enterprise_fees_helper.rb +++ b/app/helpers/enterprise_fees_helper.rb @@ -1,4 +1,12 @@ module EnterpriseFeesHelper + def angular_name(method) + "enterprise_fee_set[collection_attributes][{{ $index }}][#{method}]" + end + + def angular_id(method) + "enterprise_fee_set_collection_attributes_{{ $index }}_#{method}" + end + def enterprise_fee_type_options EnterpriseFee::FEE_TYPES.map { |f| [f.capitalize, f] } end diff --git a/app/serializers/api/admin/calculator_serializer.rb b/app/serializers/api/admin/calculator_serializer.rb new file mode 100644 index 0000000000..a6287edbb1 --- /dev/null +++ b/app/serializers/api/admin/calculator_serializer.rb @@ -0,0 +1,11 @@ +class Api::Admin::CalculatorSerializer < ActiveModel::Serializer + attributes :name, :description + + def name + object.name + end + + def description + object.description + end +end diff --git a/app/views/admin/enterprise_fees/_data.html.haml b/app/views/admin/enterprise_fees/_data.html.haml new file mode 100644 index 0000000000..0b12530bff --- /dev/null +++ b/app/views/admin/enterprise_fees/_data.html.haml @@ -0,0 +1,3 @@ += admin_inject_json_ams_array "admin.enterpriseFees", "enterprises", @enterprises, Api::Admin::IdNameSerializer += admin_inject_tax_categories(module: "admin.enterpriseFees") += admin_inject_json_ams_array "admin.enterpriseFees", "calculators", @calculators, Api::Admin::CalculatorSerializer diff --git a/app/views/admin/enterprise_fees/index.html.haml b/app/views/admin/enterprise_fees/index.html.haml index 60d818ed34..e9ab4d8650 100644 --- a/app/views/admin/enterprise_fees/index.html.haml +++ b/app/views/admin/enterprise_fees/index.html.haml @@ -3,6 +3,7 @@ = ng_form_for @enterprise_fee_set, :url => main_app.bulk_update_admin_enterprise_fees_path, :html => {'ng-app' => 'admin.enterpriseFees', 'ng-controller' => 'enterpriseFeesCtrl'} do |enterprise_fee_set_form| = hidden_field_tag 'enterprise_id', @enterprise.id if @enterprise + = render "admin/enterprise_fees/data" = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise_fee_set } %input.search{'ng-model' => 'query', 'placeholder' => 'Search'} @@ -19,14 +20,23 @@ %th.actions %tbody = enterprise_fee_set_form.ng_fields_for :collection do |f| - %tr{'ng-repeat' => 'enterprise_fee in enterprise_fees | filter:query'} + %tr{'ng-repeat' => 'enterprise_fee in enterprise_fees | filter:query' } %td = f.ng_hidden_field :id - = f.ng_collection_select :enterprise_id, @enterprises, :id, :name, 'enterprise_fee.enterprise_id', include_blank: true + %ofn-select{ :id => angular_id(:enterprise_id), data: 'enterprises', include_blank: true, ng: { model: 'enterprise_fee.enterprise_id' } } + %input{ type: "hidden", name: angular_name(:enterprise_id), ng: { value: "enterprise_fee.enterprise_id" } } + -# = f.ng_collection_select :enterprise_id, @enterprises, :id, :name, 'enterprise_fee.enterprise_id', include_blank: false %td= f.ng_select :fee_type, enterprise_fee_type_options, 'enterprise_fee.fee_type' %td= f.ng_text_field :name, { placeholder: 'e.g. packing fee' } - %td= f.ng_collection_select :tax_category_id, @tax_categories, :id, :name, 'enterprise_fee.tax_category_id', include_blank: true - %td= f.ng_collection_select :calculator_type, @calculators, :name, :description, 'enterprise_fee.calculator_type', {'class' => 'calculator_type', 'ng-model' => 'calculatorType', 'spree-ensure-calculator-preferences-match-type' => "1"} + %td + = f.ng_hidden_field :inherits_tax_category + %ofn-select{ :id => angular_id(:tax_category_id), data: 'tax_categories', include_blank: true, ng: { model: 'enterprise_fee.tax_category_id' } } + %input{ type: "hidden", name: angular_name(:tax_category_id), ng: { value: "enterprise_fee.tax_category_id" } } + -# = f.ng_collection_select :tax_category_id, @tax_categories, :id, :name, 'enterprise_fee.tax_category_id', include_blank: " " + %td + %ofn-select.calculator_type{ :id => angular_id(:calculator_type), ng: { model: 'enterprise_fee.calculator_type' }, value_attr: 'name', text_attr: 'description', data: 'calculators', 'spree-ensure-calculator-preferences-match-type' => true } + %input{ type: "hidden", name: angular_name(:calculator_type), ng: { value: "enterprise_fee.calculator_type" } } + -# = f.ng_collection_select :calculator_type, @calculators, :name, :description, 'enterprise_fee.calculator_type', {'class' => 'calculator_type', 'spree-ensure-calculator-preferences-match-type' => "1"} %td{'ng-bind-html-unsafe-compiled' => 'enterprise_fee.calculator_settings'} %td.actions{'spree-delete-resource' => "1"} diff --git a/spec/features/admin/enterprise_fees_spec.rb b/spec/features/admin/enterprise_fees_spec.rb index d462a3d809..c90691b3ac 100644 --- a/spec/features/admin/enterprise_fees_spec.rb +++ b/spec/features/admin/enterprise_fees_spec.rb @@ -17,11 +17,11 @@ feature %q{ click_link 'Configuration' click_link 'Enterprise Fees' - page.should have_selector "#enterprise_fee_set_collection_attributes_0_enterprise_id" - page.should have_selector "option[selected]", text: 'Packing' + page.should have_select "enterprise_fee_set_collection_attributes_0_enterprise_id" + page.should have_select "enterprise_fee_set_collection_attributes_0_fee_type", selected: 'Packing' page.should have_selector "input[value='$0.50 / kg']" - page.should have_selector "option[selected]", text: 'GST' - page.should have_selector "option[selected]", text: 'Flat Rate (per item)' + page.should have_select "enterprise_fee_set_collection_attributes_0_tax_category_id", selected: 'GST' + page.should have_select "enterprise_fee_set_collection_attributes_0_calculator_type", selected: 'Flat Rate (per item)' page.should have_selector "input[value='#{amount}']" end @@ -73,8 +73,8 @@ feature %q{ click_button 'Update' # Then I should see the updated fields for my fee - page.should have_selector "option[selected]", text: 'Foo' - page.should have_selector "option[selected]", text: 'Admin' + page.should have_select "enterprise_fee_set_collection_attributes_0_enterprise_id", selected: 'Foo' + page.should have_select "enterprise_fee_set_collection_attributes_0_fee_type", selected: 'Admin' page.should have_selector "input[value='Greetings!']" page.should have_select 'enterprise_fee_set_collection_attributes_0_tax_category_id', selected: '' page.should have_selector "option[selected]", text: 'Flat Percent' From caa8818f02de394fe1e2c5e72f6c9b8c4b9add87 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 4 Feb 2016 15:35:49 +1100 Subject: [PATCH 28/54] Adding option to 'Inherit From Product' for enterprise_fee tax categories --- .../enterprise_fees_controller.js.coffee | 2 +- .../directives/watch_tax_category.js.coffee | 15 +++++++++ .../admin/enterprise_fees_controller.rb | 9 ++---- app/models/enterprise_fee.rb | 2 +- app/presenters/enterprise_fee_presenter.rb | 31 ------------------- .../api/admin/enterprise_fee_serializer.rb | 4 ++- .../admin/enterprise_fees/index.html.haml | 9 ++---- app/views/admin/enterprise_fees/index.rep | 11 ------- ...nherits_tax_category_to_enterprise_fees.rb | 5 +++ db/schema.rb | 7 +++-- spec/features/admin/enterprise_fees_spec.rb | 16 ++++++++-- 11 files changed, 48 insertions(+), 63 deletions(-) create mode 100644 app/assets/javascripts/admin/enterprise_fees/directives/watch_tax_category.js.coffee delete mode 100644 app/presenters/enterprise_fee_presenter.rb delete mode 100644 app/views/admin/enterprise_fees/index.rep create mode 100644 db/migrate/20160204031816_add_inherits_tax_category_to_enterprise_fees.rb diff --git a/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee b/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee index fee2140dcc..e3cf5ee4cd 100644 --- a/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee @@ -1,6 +1,6 @@ angular.module('admin.enterpriseFees').controller 'enterpriseFeesCtrl', ($scope, $http, $window, enterprises, tax_categories, calculators) -> $scope.enterprises = enterprises - $scope.tax_categories = tax_categories + $scope.tax_categories = [{id: -1, name: "Inherit From Product"}].concat tax_categories $scope.calculators = calculators $scope.enterpriseFeesUrl = -> diff --git a/app/assets/javascripts/admin/enterprise_fees/directives/watch_tax_category.js.coffee b/app/assets/javascripts/admin/enterprise_fees/directives/watch_tax_category.js.coffee new file mode 100644 index 0000000000..370f34d9f0 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/directives/watch_tax_category.js.coffee @@ -0,0 +1,15 @@ +angular.module("admin.enterpriseFees").directive 'watchTaxCategory', -> + # In order to have a nice user experience on this page, we're modelling tax_category + # inheritance using tax_category_id = -1. + # This directive acts as a parser for tax_category_id, storing the value the form as "" when + # tax_category is to be inherited and setting inherits_tax_category as appropriate. + (scope, element, attrs) -> + scope.$watch 'enterprise_fee.tax_category_id', (value) -> + if value == -1 + scope.enterprise_fee.inherits_tax_category = true + element.val("") + else + scope.enterprise_fee.inherits_tax_category = false + element.val(value) + + scope.enterprise_fee.tax_category_id = -1 if scope.enterprise_fee.inherits_tax_category diff --git a/app/controllers/admin/enterprise_fees_controller.rb b/app/controllers/admin/enterprise_fees_controller.rb index 866c05ea54..d966a6ad8e 100644 --- a/app/controllers/admin/enterprise_fees_controller.rb +++ b/app/controllers/admin/enterprise_fees_controller.rb @@ -16,18 +16,15 @@ module Admin respond_to do |format| format.html - format.json { @presented_collection = @collection.each_with_index.map { |ef, i| EnterpriseFeePresenter.new(self, ef, i) } } + format.json { render_as_json @collection, controller: self, include_calculators: @include_calculators } + # format.json { @presented_collection = @collection.each_with_index.map { |ef, i| EnterpriseFeePresenter.new(self, ef, i) } } end end def for_order_cycle respond_to do |format| format.html - format.json do - render json: ActiveModel::ArraySerializer.new( @collection, - each_serializer: Api::Admin::EnterpriseFeeSerializer, controller: self - ).to_json - end + format.json { render_as_json @collection, controller: self } end end diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index e19f91d0f4..cf88fdcf9d 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -9,7 +9,7 @@ class EnterpriseFee < ActiveRecord::Base calculated_adjustments - attr_accessible :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type + attr_accessible :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type, :inherits_tax_category FEE_TYPES = %w(packing transport admin sales fundraising) PER_ORDER_CALCULATORS = ['Spree::Calculator::FlatRate', 'Spree::Calculator::FlexiRate'] diff --git a/app/presenters/enterprise_fee_presenter.rb b/app/presenters/enterprise_fee_presenter.rb deleted file mode 100644 index b8f9ae4655..0000000000 --- a/app/presenters/enterprise_fee_presenter.rb +++ /dev/null @@ -1,31 +0,0 @@ -class EnterpriseFeePresenter - def initialize(controller, enterprise_fee, index) - @controller, @enterprise_fee, @index = controller, enterprise_fee, index - end - - delegate :id, :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type, :to => :enterprise_fee - - def enterprise_fee - @enterprise_fee - end - - - def enterprise_name - @enterprise_fee.enterprise.andand.name - end - - def calculator_description - @enterprise_fee.calculator.andand.description - end - - def calculator_settings - result = nil - - @controller.send(:with_format, :html) do - result = @controller.render_to_string :partial => 'admin/enterprise_fees/calculator_settings', :locals => {:enterprise_fee => @enterprise_fee, :index => @index} - end - - result.gsub('[0]', '[{{ $index }}]').gsub('_0_', '_{{ $index }}_') - end - -end diff --git a/app/serializers/api/admin/enterprise_fee_serializer.rb b/app/serializers/api/admin/enterprise_fee_serializer.rb index 1b5a201b65..96c279d1c0 100644 --- a/app/serializers/api/admin/enterprise_fee_serializer.rb +++ b/app/serializers/api/admin/enterprise_fee_serializer.rb @@ -1,5 +1,5 @@ class Api::Admin::EnterpriseFeeSerializer < ActiveModel::Serializer - attributes :id, :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type + attributes :id, :enterprise_id, :fee_type, :name, :tax_category_id, :inherits_tax_category, :calculator_type attributes :enterprise_name, :calculator_description, :calculator_settings def enterprise_name @@ -11,6 +11,8 @@ class Api::Admin::EnterpriseFeeSerializer < ActiveModel::Serializer end def calculator_settings + return nil unless options[:include_calculators] + result = nil options[:controller].send(:with_format, :html) do diff --git a/app/views/admin/enterprise_fees/index.html.haml b/app/views/admin/enterprise_fees/index.html.haml index e9ab4d8650..8a1e972a33 100644 --- a/app/views/admin/enterprise_fees/index.html.haml +++ b/app/views/admin/enterprise_fees/index.html.haml @@ -20,23 +20,20 @@ %th.actions %tbody = enterprise_fee_set_form.ng_fields_for :collection do |f| - %tr{'ng-repeat' => 'enterprise_fee in enterprise_fees | filter:query' } + %tr{ ng: { repeat: 'enterprise_fee in enterprise_fees | filter:query' } } %td = f.ng_hidden_field :id %ofn-select{ :id => angular_id(:enterprise_id), data: 'enterprises', include_blank: true, ng: { model: 'enterprise_fee.enterprise_id' } } %input{ type: "hidden", name: angular_name(:enterprise_id), ng: { value: "enterprise_fee.enterprise_id" } } - -# = f.ng_collection_select :enterprise_id, @enterprises, :id, :name, 'enterprise_fee.enterprise_id', include_blank: false %td= f.ng_select :fee_type, enterprise_fee_type_options, 'enterprise_fee.fee_type' %td= f.ng_text_field :name, { placeholder: 'e.g. packing fee' } %td - = f.ng_hidden_field :inherits_tax_category %ofn-select{ :id => angular_id(:tax_category_id), data: 'tax_categories', include_blank: true, ng: { model: 'enterprise_fee.tax_category_id' } } - %input{ type: "hidden", name: angular_name(:tax_category_id), ng: { value: "enterprise_fee.tax_category_id" } } - -# = f.ng_collection_select :tax_category_id, @tax_categories, :id, :name, 'enterprise_fee.tax_category_id', include_blank: " " + %input{ type: "hidden", name: angular_name(:tax_category_id), 'watch-tax-category' => true } + %input{ type: "hidden", name: angular_name(:inherits_tax_category), ng: { value: "enterprise_fee.inherits_tax_category" } } %td %ofn-select.calculator_type{ :id => angular_id(:calculator_type), ng: { model: 'enterprise_fee.calculator_type' }, value_attr: 'name', text_attr: 'description', data: 'calculators', 'spree-ensure-calculator-preferences-match-type' => true } %input{ type: "hidden", name: angular_name(:calculator_type), ng: { value: "enterprise_fee.calculator_type" } } - -# = f.ng_collection_select :calculator_type, @calculators, :name, :description, 'enterprise_fee.calculator_type', {'class' => 'calculator_type', 'spree-ensure-calculator-preferences-match-type' => "1"} %td{'ng-bind-html-unsafe-compiled' => 'enterprise_fee.calculator_settings'} %td.actions{'spree-delete-resource' => "1"} diff --git a/app/views/admin/enterprise_fees/index.rep b/app/views/admin/enterprise_fees/index.rep deleted file mode 100644 index 8dc24b5d74..0000000000 --- a/app/views/admin/enterprise_fees/index.rep +++ /dev/null @@ -1,11 +0,0 @@ -r.list_of :enterprise_fees, @presented_collection do - r.element :id - r.element :enterprise_id - r.element :enterprise_name - r.element :fee_type - r.element :name - r.element :tax_category_id - r.element :calculator_type - r.element :calculator_description - r.element :calculator_settings if @include_calculators -end diff --git a/db/migrate/20160204031816_add_inherits_tax_category_to_enterprise_fees.rb b/db/migrate/20160204031816_add_inherits_tax_category_to_enterprise_fees.rb new file mode 100644 index 0000000000..49751cc479 --- /dev/null +++ b/db/migrate/20160204031816_add_inherits_tax_category_to_enterprise_fees.rb @@ -0,0 +1,5 @@ +class AddInheritsTaxCategoryToEnterpriseFees < ActiveRecord::Migration + def change + add_column :enterprise_fees, :inherits_tax_category, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 465530b949..9911b562c9 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 => 20151128185900) do +ActiveRecord::Schema.define(:version => 20160204031816) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -235,9 +235,10 @@ ActiveRecord::Schema.define(:version => 20151128185900) do t.integer "enterprise_id" t.string "fee_type" t.string "name" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "tax_category_id" + t.boolean "inherits_tax_category", :default => false, :null => false end add_index "enterprise_fees", ["enterprise_id"], :name => "index_enterprise_fees_on_enterprise_id" diff --git a/spec/features/admin/enterprise_fees_spec.rb b/spec/features/admin/enterprise_fees_spec.rb index c90691b3ac..7025b54766 100644 --- a/spec/features/admin/enterprise_fees_spec.rb +++ b/spec/features/admin/enterprise_fees_spec.rb @@ -57,7 +57,7 @@ feature %q{ scenario "editing an enterprise fee" do # Given an enterprise fee fee = create(:enterprise_fee) - create(:enterprise, name: 'Foo') + enterprise = create(:enterprise, name: 'Foo') # When I go to the enterprise fees page login_to_admin_section @@ -68,7 +68,7 @@ feature %q{ select 'Foo', from: 'enterprise_fee_set_collection_attributes_0_enterprise_id' select 'Admin', from: 'enterprise_fee_set_collection_attributes_0_fee_type' fill_in 'enterprise_fee_set_collection_attributes_0_name', with: 'Greetings!' - select '', from: 'enterprise_fee_set_collection_attributes_0_tax_category_id' + select 'Inherit From Product', from: 'enterprise_fee_set_collection_attributes_0_tax_category_id' select 'Flat Percent', from: 'enterprise_fee_set_collection_attributes_0_calculator_type' click_button 'Update' @@ -76,8 +76,18 @@ feature %q{ page.should have_select "enterprise_fee_set_collection_attributes_0_enterprise_id", selected: 'Foo' page.should have_select "enterprise_fee_set_collection_attributes_0_fee_type", selected: 'Admin' page.should have_selector "input[value='Greetings!']" - page.should have_select 'enterprise_fee_set_collection_attributes_0_tax_category_id', selected: '' + page.should have_select 'enterprise_fee_set_collection_attributes_0_tax_category_id', selected: 'Inherit From Product' page.should have_selector "option[selected]", text: 'Flat Percent' + + fee.reload + fee.enterprise.should == enterprise + fee.name.should == 'Greetings!' + fee.fee_type.should == 'admin' + fee.calculator_type.should == "Spree::Calculator::FlatPercentItemTotal" + + # Sets tax_category and inherits_tax_category + fee.tax_category.should == nil + fee.inherits_tax_category.should == true end scenario "deleting an enterprise fee" do From 59745fbc735450dccde7377b64385d4a3ccccbf8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 5 Feb 2016 09:26:27 +1100 Subject: [PATCH 29/54] EnterpriseFees can inherit tax_category from product --- app/models/enterprise_fee.rb | 15 ++ .../spree/calculator/default_tax_decorator.rb | 3 +- .../enterprise_fee_applicator.rb | 16 +- spec/models/enterprise_fee_spec.rb | 30 ++++ spec/models/spree/adjustment_spec.rb | 164 +++++++++++++----- 5 files changed, 179 insertions(+), 49 deletions(-) diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index cf88fdcf9d..59b2648d30 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -18,6 +18,7 @@ class EnterpriseFee < ActiveRecord::Base validates_inclusion_of :fee_type, :in => FEE_TYPES validates_presence_of :name + before_save :ensure_valid_tax_category_settings scope :for_enterprise, lambda { |enterprise| where(enterprise_id: enterprise) } scope :for_enterprises, lambda { |enterprises| where(enterprise_id: enterprises) } @@ -57,4 +58,18 @@ class EnterpriseFee < ActiveRecord::Base :mandatory => mandatory, :locked => true}, :without_protection => true) end + + private + + def ensure_valid_tax_category_settings + # Setting an explicit tax_category removes any inheritance behaviour + # In the absence of any current changes to tax_category, setting + # inherits_tax_category to true will clear the tax_category + if tax_category_id_changed? + self.inherits_tax_category = false if tax_category.present? + elsif inherits_tax_category_changed? + self.tax_category_id = nil if inherits_tax_category? + end + return true + end end diff --git a/app/models/spree/calculator/default_tax_decorator.rb b/app/models/spree/calculator/default_tax_decorator.rb index e55cfc5536..623f2df26d 100644 --- a/app/models/spree/calculator/default_tax_decorator.rb +++ b/app/models/spree/calculator/default_tax_decorator.rb @@ -19,7 +19,8 @@ Spree::Calculator::DefaultTax.class_eval do # Added this block, finds relevant fees for each line_item, calculates the tax on them, and returns the total tax per_item_fees_total = order.line_items.sum do |line_item| calculator.send(:per_item_enterprise_fee_applicators_for, line_item.variant) - .select { |applicator| applicator.enterprise_fee.tax_category == rate.tax_category } + .select { |applicator| (!applicator.enterprise_fee.inherits_tax_category && applicator.enterprise_fee.tax_category == rate.tax_category) || + (applicator.enterprise_fee.inherits_tax_category && line_item.product.tax_category == rate.tax_category) } .sum { |applicator| applicator.enterprise_fee.compute_amount(line_item) } end diff --git a/lib/open_food_network/enterprise_fee_applicator.rb b/lib/open_food_network/enterprise_fee_applicator.rb index 26b1397193..06e71dd21c 100644 --- a/lib/open_food_network/enterprise_fee_applicator.rb +++ b/lib/open_food_network/enterprise_fee_applicator.rb @@ -5,7 +5,7 @@ module OpenFoodNetwork AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role - a.set_absolute_included_tax! adjustment_tax(line_item.order, a) + a.set_absolute_included_tax! adjustment_tax(line_item, a) end def create_order_adjustment(order) @@ -31,12 +31,22 @@ module OpenFoodNetwork "#{enterprise_fee.fee_type} fee by #{role} #{enterprise_fee.enterprise.name}" end - def adjustment_tax(order, adjustment) - tax_rates = enterprise_fee.tax_category ? enterprise_fee.tax_category.tax_rates.match(order) : [] + def adjustment_tax(adjustable, adjustment) + tax_rates = rates_for(adjustable) tax_rates.select(&:included_in_price).sum do |rate| rate.compute_tax adjustment.amount end end + + def rates_for(adjustable) + case adjustable + when Spree::LineItem + tax_category = enterprise_fee.inherits_tax_category? ? adjustable.product.tax_category : enterprise_fee.tax_category + return tax_category ? tax_category.tax_rates.match(adjustable.order) : [] + when Spree::Order + return enterprise_fee.tax_category ? enterprise_fee.tax_category.tax_rates.match(adjustable) : [] + end + end end end diff --git a/spec/models/enterprise_fee_spec.rb b/spec/models/enterprise_fee_spec.rb index f0f9df1f0b..7cfc0a5783 100644 --- a/spec/models/enterprise_fee_spec.rb +++ b/spec/models/enterprise_fee_spec.rb @@ -26,6 +26,36 @@ describe EnterpriseFee do ef.destroy ex.reload.exchange_fee_ids.should be_empty end + + describe "for tax_category" do + let(:tax_category) { create(:tax_category) } + let(:enterprise_fee) { create(:enterprise_fee, tax_category_id: nil, inherits_tax_category: true) } + + + it "maintains valid tax_category settings" do + # Changing just tax_category, when inheriting + # tax_category is changed, inherits.. set to false + enterprise_fee.assign_attributes(tax_category_id: tax_category.id) + enterprise_fee.save! + expect(enterprise_fee.tax_category).to eq tax_category + expect(enterprise_fee.inherits_tax_category).to be false + + # Changing inherits_tax_category, when tax_category is set + # tax_category is dropped, inherits.. set to true + enterprise_fee.assign_attributes(inherits_tax_category: true) + enterprise_fee.save! + expect(enterprise_fee.tax_category).to be nil + expect(enterprise_fee.inherits_tax_category).to be true + + # Changing both tax_category and inherits_tax_category + # tax_category is changed, but inherits.. changes are dropped + enterprise_fee.assign_attributes(tax_category_id: tax_category.id) + enterprise_fee.assign_attributes(inherits_tax_category: true) + enterprise_fee.save! + expect(enterprise_fee.tax_category).to eq tax_category + expect(enterprise_fee.inherits_tax_category).to be false + end + end end describe "scopes" do diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index cd49e3893a..c3e2ae8304 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -120,78 +120,152 @@ module Spree describe "EnterpriseFee adjustments" do let!(:zone) { create(:zone_with_member) } - let(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::DefaultTax.new, zone: zone, amount: 0.1) } - let(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) } + let(:fee_tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::DefaultTax.new, zone: zone, amount: 0.1) } + let(:fee_tax_category) { create(:tax_category, tax_rates: [fee_tax_rate]) } let(:coordinator) { create(:distributor_enterprise, charges_sales_tax: true) } - let(:variant) { create(:variant) } + let(:variant) { create(:variant, product: create(:product, tax_category: nil)) } let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator, coordinator_fees: [enterprise_fee], distributors: [coordinator], variants: [variant]) } let!(:order) { create(:order, order_cycle: order_cycle, distributor: coordinator) } let!(:line_item) { create(:line_item, order: order, variant: variant) } let(:adjustment) { order.adjustments(:reload).enterprise_fee.first } - before do - order.reload.update_distribution_charge! - end + context "when enterprise fees have a fixed tax_category" do + before do + order.reload.update_distribution_charge! + end - context "when enterprise fees are taxed per-order" do - let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: tax_category, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) } + context "when enterprise fees are taxed per-order" do + let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: fee_tax_category, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) } - describe "when the tax rate includes the tax in the price" do - it "records the tax on the enterprise fee adjustments" do - # The fee is $50, tax is 10%, and the fee is inclusive of tax - # Therefore, the included tax should be 0.1/1.1 * 50 = $4.55 + describe "when the tax rate includes the tax in the price" do + it "records the tax on the enterprise fee adjustments" do + # The fee is $50, tax is 10%, and the fee is inclusive of tax + # Therefore, the included tax should be 0.1/1.1 * 50 = $4.55 - adjustment.included_tax.should == 4.55 + adjustment.included_tax.should == 4.55 + end + end + + describe "when the tax rate does not include the tax in the price" do + before do + fee_tax_rate.update_attribute :included_in_price, false + order.reload.create_tax_charge! # Updating line_item or order has the same effect + order.update_distribution_charge! + end + + it "records the tax on TaxRate adjustment on the order" do + adjustment.included_tax.should == 0 + order.adjustments.tax.first.amount.should == 5.0 + end + end + + describe "when enterprise fees have no tax" do + before do + enterprise_fee.tax_category = nil + enterprise_fee.save! + order.update_distribution_charge! + end + + it "records no tax as charged" do + adjustment.included_tax.should == 0 + end end end - describe "when the tax rate does not include the tax in the price" do - before do - tax_rate.update_attribute :included_in_price, false - order.create_tax_charge! # Updating line_item or order has the same effect - order.update_distribution_charge! + + context "when enterprise fees are taxed per-item" do + let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: fee_tax_category, calculator: Calculator::PerItem.new(preferred_amount: 50.0)) } + + describe "when the tax rate includes the tax in the price" do + it "records the tax on the enterprise fee adjustments" do + adjustment.included_tax.should == 4.55 + end end - it "records the tax on TaxRate adjustment on the order" do - adjustment.included_tax.should == 0 - order.adjustments.tax.first.amount.should == 5.0 - end - end + describe "when the tax rate does not include the tax in the price" do + before do + fee_tax_rate.update_attribute :included_in_price, false + order.reload.create_tax_charge! # Updating line_item or order has the same effect + order.update_distribution_charge! + end - describe "when enterprise fees have no tax" do - before do - enterprise_fee.tax_category = nil - enterprise_fee.save! - order.update_distribution_charge! - end - - it "records no tax as charged" do - adjustment.included_tax.should == 0 + it "records the tax on TaxRate adjustment on the order" do + adjustment.included_tax.should == 0 + order.adjustments.tax.first.amount.should == 5.0 + end end end end + context "when enterprise fees inherit their tax_category product they are applied to" do + let(:product_tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::DefaultTax.new, zone: zone, amount: 0.2) } + let(:product_tax_category) { create(:tax_category, tax_rates: [product_tax_rate]) } - context "when enterprise fees are taxed per-item" do - let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: tax_category, calculator: Calculator::PerItem.new(preferred_amount: 50.0)) } + before do + variant.product.update_attribute(:tax_category_id, product_tax_category.id) + order.reload.create_tax_charge! # Updating line_item or order has the same effect + order.reload.update_distribution_charge! + end - describe "when the tax rate includes the tax in the price" do - it "records the tax on the enterprise fee adjustments" do - adjustment.included_tax.should == 4.55 + context "when enterprise fees are taxed per-order" do + let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, inherits_tax_category: true, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) } + + describe "when the tax rate includes the tax in the price" do + it "records no tax on the enterprise fee adjustments" do + # EnterpriseFee tax category is nil and inheritance only applies to per item fees + # so tax on the enterprise_fee adjustment will be 0 + # Tax on line item is: 0.2/1.2 x $10 = $1.67 + adjustment.included_tax.should == 0.0 + line_item.adjustments.first.included_tax.should == 1.67 + end + end + + describe "when the tax rate does not include the tax in the price" do + before do + product_tax_rate.update_attribute :included_in_price, false + order.reload.create_tax_charge! # Updating line_item or order has the same effect + order.reload.update_distribution_charge! + end + + it "records the no tax on TaxRate adjustment on the order" do + # EnterpriseFee tax category is nil and inheritance only applies to per item fees + # so total tax on the order is only that which applies to the line_item itself + # ie. $10 x 0.2 = $2.0 + adjustment.included_tax.should == 0 + order.adjustments.tax.first.amount.should == 2.0 + end end end - describe "when the tax rate does not include the tax in the price" do - before do - tax_rate.update_attribute :included_in_price, false - order.create_tax_charge! # Updating line_item or order has the same effect - order.update_distribution_charge! + + context "when enterprise fees are taxed per-item" do + let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, inherits_tax_category: true, calculator: Calculator::PerItem.new(preferred_amount: 50.0)) } + + describe "when the tax rate includes the tax in the price" do + it "records the tax on the enterprise fee adjustments" do + # Applying product tax rate of 0.2 to enterprise fee of $50 + # gives tax on fee of 0.2/1.2 x $50 = $8.33 + # Tax on line item is: 0.2/1.2 x $10 = $1.67 + adjustment.included_tax.should == 8.33 + line_item.adjustments.first.included_tax.should == 1.67 + end end - it "records the tax on TaxRate adjustment on the order" do - adjustment.included_tax.should == 0 - order.adjustments.tax.first.amount.should == 5.0 + describe "when the tax rate does not include the tax in the price" do + before do + product_tax_rate.update_attribute :included_in_price, false + order.reload.create_tax_charge! # Updating line_item or order has the same effect + order.update_distribution_charge! + end + + it "records the tax on TaxRate adjustment on the order" do + # EnterpriseFee inherits tax_category from product so total tax on + # the order is that which applies to the line item itself, plus the + # same rate applied to the fee of $50. ie. ($10 + $50) x 0.2 = $12.0 + adjustment.included_tax.should == 0 + order.adjustments.tax.first.amount.should == 12.0 + end end end end From 095b420997c44574f18614bf9e973867d0229417 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 15 Jan 2016 14:20:10 +1100 Subject: [PATCH 30/54] WIP: Adding InventoryItem model for managing contents of inventories --- app/models/enterprise.rb | 1 + app/models/inventory_item.rb | 11 +++++++++++ db/migrate/20160114001844_create_inventory_items.rb | 13 +++++++++++++ db/schema.rb | 12 +++++++++++- 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 app/models/inventory_item.rb create mode 100644 db/migrate/20160114001844_create_inventory_items.rb diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 34a5d22f5b..66f990177d 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -36,6 +36,7 @@ class Enterprise < ActiveRecord::Base has_many :shipping_methods, through: :distributor_shipping_methods has_many :customers has_many :billable_periods + has_many :inventory_items delegate :latitude, :longitude, :city, :state_name, :to => :address diff --git a/app/models/inventory_item.rb b/app/models/inventory_item.rb new file mode 100644 index 0000000000..b9aed9899a --- /dev/null +++ b/app/models/inventory_item.rb @@ -0,0 +1,11 @@ +class InventoryItem < ActiveRecord::Base + attr_accessible :enterprise_id, :variant_id, :visible + + belongs_to :enterprise + belongs_to :variant, class_name: "Spree::Variant" + + validates :variant_id, uniqueness: { scope: :enterprise_id } + + scope :visible, where(visible: true) + scope :hidden, where(visible: false) +end diff --git a/db/migrate/20160114001844_create_inventory_items.rb b/db/migrate/20160114001844_create_inventory_items.rb new file mode 100644 index 0000000000..2ca54dce4d --- /dev/null +++ b/db/migrate/20160114001844_create_inventory_items.rb @@ -0,0 +1,13 @@ +class CreateInventoryItems < ActiveRecord::Migration + def change + create_table :inventory_items do |t| + t.references :enterprise, null: false, index: true + t.references :variant, null: false, index: true + t.boolean :visible, default: true, null: false + + t.timestamps + end + + add_index "inventory_items", [:enterprise_id, :variant_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 9911b562c9..ac02e75ff4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -395,6 +395,16 @@ ActiveRecord::Schema.define(:version => 20160204031816) do add_index "exchanges", ["receiver_id"], :name => "index_exchanges_on_receiver_id" add_index "exchanges", ["sender_id"], :name => "index_exchanges_on_sender_id" + create_table "inventory_items", :force => true do |t| + t.integer "enterprise_id", :null => false + t.integer "variant_id", :null => false + t.boolean "visible", :default => true, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "inventory_items", ["enterprise_id", "variant_id"], :name => "index_inventory_items_on_enterprise_id_and_variant_id", :unique => true + create_table "order_cycles", :force => true do |t| t.string "name" t.datetime "orders_open_at" @@ -671,9 +681,9 @@ ActiveRecord::Schema.define(:version => 20160204031816) 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" t.integer "customer_id" end From f06d909c23e23104485e923370c709095963bde9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 15 Jan 2016 18:09:07 +1100 Subject: [PATCH 31/54] WIP: Adding infrastructure to show/hide variants + overrides using inventory items --- app/assets/javascripts/admin/all.js | 1 + .../inventory_items/inventory_items.js.coffee | 1 + .../inventory_item_resource.js.coffee | 5 + .../services/inventory_items.js.coffee | 25 ++ .../variant_overrides_controller.js.coffee | 7 +- .../inventory_products_filter.js.coffee | 12 + .../inventory_variants_filter.js.coffee | 12 + .../new_inventory_products_filter.js.coffee | 8 + .../new_inventory_variants_filter.js.coffee | 7 + .../variant_overrides.js.coffee | 2 +- .../admin/components/save_bar.sass | 4 + .../admin/inventory_items_controller.rb | 28 ++ .../admin/variant_overrides_controller.rb | 2 + app/helpers/admin/injection_helper.rb | 4 + app/models/inventory_item.rb | 3 + app/models/spree/ability_decorator.rb | 14 + .../api/admin/inventory_item_serializer.rb | 3 + .../admin/variant_overrides/_data.html.haml | 1 + .../variant_overrides/_new_variants.html.haml | 25 ++ .../variant_overrides/_products.html.haml | 11 +- .../_products_product.html.haml | 1 + .../_products_variants.html.haml | 4 +- .../admin/variant_overrides/index.html.haml | 6 +- .../admin/orders/bulk_management.html.haml | 2 +- config/routes.rb | 2 + .../admin/inventory_items_controller_spec.rb | 129 ++++++ .../variant_overrides_controller_spec.rb | 17 + spec/factories.rb | 6 + .../admin/bulk_order_management_spec.rb | 4 + spec/features/admin/variant_overrides_spec.rb | 405 ++++++++++-------- ...ariant_overrides_controller_spec.js.coffee | 2 + .../services/inventory_items_spec.js.coffee | 73 ++++ 32 files changed, 643 insertions(+), 183 deletions(-) create mode 100644 app/assets/javascripts/admin/inventory_items/inventory_items.js.coffee create mode 100644 app/assets/javascripts/admin/inventory_items/services/inventory_item_resource.js.coffee create mode 100644 app/assets/javascripts/admin/inventory_items/services/inventory_items.js.coffee create mode 100644 app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee create mode 100644 app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee create mode 100644 app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee create mode 100644 app/assets/javascripts/admin/variant_overrides/filters/new_inventory_variants_filter.js.coffee create mode 100644 app/controllers/admin/inventory_items_controller.rb create mode 100644 app/serializers/api/admin/inventory_item_serializer.rb create mode 100644 app/views/admin/variant_overrides/_new_variants.html.haml create mode 100644 spec/controllers/admin/inventory_items_controller_spec.rb create mode 100644 spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 3485f45e23..344c5e8a5d 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -30,6 +30,7 @@ //= require ./enterprise_fees/enterprise_fees //= require ./enterprise_groups/enterprise_groups //= require ./index_utils/index_utils +//= require ./inventory_items/inventory_items //= require ./line_items/line_items //= require ./orders/orders //= require ./order_cycles/order_cycles diff --git a/app/assets/javascripts/admin/inventory_items/inventory_items.js.coffee b/app/assets/javascripts/admin/inventory_items/inventory_items.js.coffee new file mode 100644 index 0000000000..5ee31bad4b --- /dev/null +++ b/app/assets/javascripts/admin/inventory_items/inventory_items.js.coffee @@ -0,0 +1 @@ +angular.module("admin.inventoryItems", ['ngResource']) diff --git a/app/assets/javascripts/admin/inventory_items/services/inventory_item_resource.js.coffee b/app/assets/javascripts/admin/inventory_items/services/inventory_item_resource.js.coffee new file mode 100644 index 0000000000..fb2699f854 --- /dev/null +++ b/app/assets/javascripts/admin/inventory_items/services/inventory_item_resource.js.coffee @@ -0,0 +1,5 @@ +angular.module("admin.inventoryItems").factory 'InventoryItemResource', ($resource) -> + $resource('/admin/inventory_items/:id/:action.json', {}, { + 'update': + method: 'PUT' + }) diff --git a/app/assets/javascripts/admin/inventory_items/services/inventory_items.js.coffee b/app/assets/javascripts/admin/inventory_items/services/inventory_items.js.coffee new file mode 100644 index 0000000000..ac7971ad24 --- /dev/null +++ b/app/assets/javascripts/admin/inventory_items/services/inventory_items.js.coffee @@ -0,0 +1,25 @@ +angular.module("admin.inventoryItems").factory "InventoryItems", (inventoryItems, InventoryItemResource) -> + new class InventoryItems + inventoryItems: {} + errors: {} + + constructor: -> + for ii in inventoryItems + @inventoryItems[ii.enterprise_id] ||= {} + @inventoryItems[ii.enterprise_id][ii.variant_id] = new InventoryItemResource(ii) + + setVisibility: (hub_id, variant_id, visible) -> + if @inventoryItems[hub_id] && @inventoryItems[hub_id][variant_id] + inventory_item = angular.extend(angular.copy(@inventoryItems[hub_id][variant_id]), {visible: visible}) + InventoryItemResource.update {id: inventory_item.id}, inventory_item, (data) => + @inventoryItems[hub_id][variant_id] = data + , (response) => + @errors[hub_id] ||= {} + @errors[hub_id][variant_id] = response.data.errors + else + InventoryItemResource.save {enterprise_id: hub_id, variant_id: variant_id, visible: visible}, (data) => + @inventoryItems[hub_id] ||= {} + @inventoryItems[hub_id][variant_id] = data + , (response) => + @errors[hub_id] ||= {} + @errors[hub_id][variant_id] = response.data.errors diff --git a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee index b65df80d66..c7743efa4f 100644 --- a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee @@ -1,11 +1,15 @@ -angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides, DirtyVariantOverrides) -> +angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, InventoryItems, VariantOverrides, DirtyVariantOverrides) -> $scope.hubs = Indexer.index hubs $scope.hub = null $scope.products = [] $scope.producers = producers $scope.producersByID = Indexer.index producers $scope.hubPermissions = hubPermissions + $scope.showHidden = false + $scope.productLimit = 10 $scope.variantOverrides = VariantOverrides.variantOverrides + $scope.inventoryItems = InventoryItems.inventoryItems + $scope.setVisibility = InventoryItems.setVisibility $scope.StatusMessage = StatusMessage $scope.columns = Columns.setColumns @@ -17,6 +21,7 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", on_demand: { name: "On Demand", visible: false } reset: { name: "Reset Stock Level", visible: false } inheritance: { name: "Inheritance", visible: false } + visibility: { name: "Show/Hide", visible: false } $scope.resetSelectFilters = -> $scope.producerFilter = 0 diff --git a/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee new file mode 100644 index 0000000000..4f5a3c5191 --- /dev/null +++ b/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee @@ -0,0 +1,12 @@ +angular.module("admin.variantOverrides").filter "inventoryProducts", ($filter, InventoryItems) -> + return (products, hub_id, showHidden) -> + return [] if !hub_id + return $filter('filter')(products, (product) -> + for variant in product.variants + if InventoryItems.inventoryItems.hasOwnProperty(hub_id) && InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) + if showHidden + return true + else + return true if InventoryItems.inventoryItems[hub_id][variant.id].visible + false + , true) diff --git a/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee new file mode 100644 index 0000000000..51238fa189 --- /dev/null +++ b/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee @@ -0,0 +1,12 @@ +angular.module("admin.variantOverrides").filter "inventoryVariants", ($filter, InventoryItems) -> + return (variants, hub_id, showHidden) -> + return [] if !hub_id + return $filter('filter')(variants, (variant) -> + if InventoryItems.inventoryItems.hasOwnProperty(hub_id) && InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) + if showHidden + return true + else + return InventoryItems.inventoryItems[hub_id][variant.id].visible + else + false + , true) diff --git a/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee new file mode 100644 index 0000000000..ec1148c22e --- /dev/null +++ b/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.variantOverrides").filter "newInventoryProducts", ($filter, InventoryItems) -> + return (products, hub_id) -> + return [] if !hub_id + return products unless InventoryItems.inventoryItems.hasOwnProperty(hub_id) + return $filter('filter')(products, (product) -> + for variant in product.variants + return true if !InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) + , true) diff --git a/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_variants_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_variants_filter.js.coffee new file mode 100644 index 0000000000..d06724a704 --- /dev/null +++ b/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_variants_filter.js.coffee @@ -0,0 +1,7 @@ +angular.module("admin.variantOverrides").filter "newInventoryVariants", ($filter, InventoryItems) -> + return (variants, hub_id) -> + return [] if !hub_id + return variants unless InventoryItems.inventoryItems.hasOwnProperty(hub_id) + return $filter('filter')(variants, (variant) -> + !InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) + , true) diff --git a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee index ae46cd14c7..c302c0463e 100644 --- a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee @@ -1 +1 @@ -angular.module("admin.variantOverrides", ["pasvaz.bindonce", "admin.indexUtils", "admin.utils", "admin.dropdown"]) +angular.module("admin.variantOverrides", ["pasvaz.bindonce", "admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems"]) diff --git a/app/assets/stylesheets/admin/components/save_bar.sass b/app/assets/stylesheets/admin/components/save_bar.sass index c6b1236490..3fdeb47339 100644 --- a/app/assets/stylesheets/admin/components/save_bar.sass +++ b/app/assets/stylesheets/admin/components/save_bar.sass @@ -7,3 +7,7 @@ color: #5498da h5 color: #5498da + +// Making space for save-bar +.margin-bottom-50 + margin-bottom: 50px diff --git a/app/controllers/admin/inventory_items_controller.rb b/app/controllers/admin/inventory_items_controller.rb new file mode 100644 index 0000000000..cb649fe6f2 --- /dev/null +++ b/app/controllers/admin/inventory_items_controller.rb @@ -0,0 +1,28 @@ +module Admin + class InventoryItemsController < ResourceController + + respond_to :json + + respond_override update: { json: { + success: lambda { sleep 3; render_as_json @inventory_item }, + failure: lambda { render json: { errors: @inventory_item.errors.full_messages }, status: :unprocessable_entity } + } } + + respond_override create: { json: { + success: lambda { sleep 3; render_as_json @inventory_item }, + failure: lambda { render json: { errors: @inventory_item.errors.full_messages }, status: :unprocessable_entity } + } } + + private + + # Overriding Spree method to load data from params here so that + # we can authorise #create using an object with required attributes + def build_resource + if parent_data.present? + parent.send(controller_name).build + else + model_class.new(params[object_name]) # This line changed + end + end + end +end diff --git a/app/controllers/admin/variant_overrides_controller.rb b/app/controllers/admin/variant_overrides_controller.rb index c886f80652..446ca457eb 100644 --- a/app/controllers/admin/variant_overrides_controller.rb +++ b/app/controllers/admin/variant_overrides_controller.rb @@ -54,6 +54,8 @@ module Admin @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). variant_override_enterprises_per_hub + + @inventory_items = InventoryItem.where(enterprise_id: @hubs) end def load_collection diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index ba65e37860..511eced707 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -39,6 +39,10 @@ module Admin admin_inject_json_ams_array opts[:module], "producers", @producers, Api::Admin::IdNameSerializer end + def admin_inject_inventory_items(opts={module: 'ofn.admin'}) + admin_inject_json_ams_array opts[:module], "inventoryItems", @inventory_items, Api::Admin::InventoryItemSerializer + end + def admin_inject_enterprise_permissions permissions = {can_manage_shipping_methods: can?(:manage_shipping_methods, @enterprise), diff --git a/app/models/inventory_item.rb b/app/models/inventory_item.rb index b9aed9899a..05198b6dec 100644 --- a/app/models/inventory_item.rb +++ b/app/models/inventory_item.rb @@ -5,6 +5,9 @@ class InventoryItem < ActiveRecord::Base belongs_to :variant, class_name: "Spree::Variant" validates :variant_id, uniqueness: { scope: :enterprise_id } + validates :enterprise_id, presence: true + validates :variant_id, presence: true + validates :visible, inclusion: { in: [true, false], message: "must be true or false" } scope :visible, where(visible: true) scope :hidden, where(visible: false) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 9bc303116b..8c93fe4b71 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -125,6 +125,20 @@ class AbilityDecorator hub_auth && producer_auth end + can [:admin, :create, :update], InventoryItem do |ii| + next false unless ii.enterprise.present? && ii.variant.andand.product.andand.supplier.present? + + hub_auth = OpenFoodNetwork::Permissions.new(user). + variant_override_hubs. + include? ii.enterprise + + producer_auth = OpenFoodNetwork::Permissions.new(user). + variant_override_producers. + include? ii.variant.product.supplier + + hub_auth && producer_auth + 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/serializers/api/admin/inventory_item_serializer.rb b/app/serializers/api/admin/inventory_item_serializer.rb new file mode 100644 index 0000000000..15f8d35058 --- /dev/null +++ b/app/serializers/api/admin/inventory_item_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::InventoryItemSerializer < ActiveModel::Serializer + attributes :id, :enterprise_id, :variant_id, :visible +end diff --git a/app/views/admin/variant_overrides/_data.html.haml b/app/views/admin/variant_overrides/_data.html.haml index 64a7619ea7..9d371415fe 100644 --- a/app/views/admin/variant_overrides/_data.html.haml +++ b/app/views/admin/variant_overrides/_data.html.haml @@ -3,3 +3,4 @@ = admin_inject_hub_permissions = admin_inject_producers module: 'admin.variantOverrides' = admin_inject_variant_overrides += admin_inject_inventory_items module: 'admin.variantOverrides' diff --git a/app/views/admin/variant_overrides/_new_variants.html.haml b/app/views/admin/variant_overrides/_new_variants.html.haml new file mode 100644 index 0000000000..ed87bc3e86 --- /dev/null +++ b/app/views/admin/variant_overrides/_new_variants.html.haml @@ -0,0 +1,25 @@ +%hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'newProducts.length > 0' } } +%table#new-variants{ ng: { show: 'newProducts.length > 0' } } + %col.producer{ width: "20%" } + %col.product{ width: "20%" } + %col.variant{ width: "40%" } + %col.add{ width: "10%" } + %col.ignore{ width: "10%" } + %thead + %tr + %th.producer Producer + %th.product Product + %th.variant Variant + %th.add Add + %th.ignore Ignore + %tbody{ bindonce: true, ng: { repeat: 'product in newProducts = (products | hubPermissions:hubPermissions:hub.id | newInventoryProducts:hub.id)' } } + %tr{ id: "nv_{{variant.id}}", ng: { repeat: 'variant in product.variants | newInventoryVariants:hub.id'} } + %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } + %td.product{ bo: { bind: 'product.name'} } + %td.variant + %span{ bo: { bind: 'variant.display_name || ""'} } + .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } + %td.add + %input.fullwidth{ :type => 'button', value: "Add", ng: { click: "setVisibility(hub.id,variant.id,true)" } } + %td.ignore + %input.fullwidth{ :type => 'button', value: "Ignore", ng: { click: "setVisibility(hub.id,variant.id,false)" } } diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index ed3354de9a..e34f6eccaa 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -1,4 +1,4 @@ -%table.index.bulk{ ng: {show: 'hub'}} +%table.index.bulk#variant-overrides{ ng: {show: 'hub'}} %col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } } %col.product{ width: "20%", ng: { show: 'columns.product.visible' } } %col.sku{ width: "20%", ng: { show: 'columns.sku.visible' } } @@ -8,6 +8,7 @@ %col.reset{ width: "1%", ng: { show: 'columns.reset.visible' } } %col.reset{ width: "15%", ng: { show: 'columns.reset.visible' } } %col.inheritance{ width: "5%", ng: { show: 'columns.inheritance.visible' } } + %col.visibility{ width: "10%", ng: { show: 'columns.visibility.visible' } } %thead %tr{ ng: { controller: "ColumnsCtrl" } } %th.producer{ ng: { show: 'columns.producer.visible' } } Producer @@ -18,6 +19,12 @@ %th.on_demand{ ng: { show: 'columns.on_demand.visible' } } On Demand? %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } } Enable Stock Level Reset? %th.inheritance{ ng: { show: 'columns.inheritance.visible' } } Inherit? - %tbody{bindonce: true, ng: {repeat: 'product in products | hubPermissions:hubPermissions:hub.id | attrFilter:{producer_id:producerFilter} | filter:query' } } + %th.visibility{ ng: { show: 'columns.visibility.visible' } } Show/Hide? + %tbody{bindonce: true, ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub.id | inventoryProducts:hub.id:showHidden | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } = render 'admin/variant_overrides/products_product' = render 'admin/variant_overrides/products_variants' + +.sixteen.columns.alpha.omega.text-center{ ng: {show: 'hub && productLimit < filteredProducts.length'}} + %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } + or + %input{ type: 'button', value: "Show All ({{ filteredProducts.length - productLimit }})", ng: { click: 'productLimit = filteredProducts.length' } } diff --git a/app/views/admin/variant_overrides/_products_product.html.haml b/app/views/admin/variant_overrides/_products_product.html.haml index b7cb11041b..70b48e3909 100644 --- a/app/views/admin/variant_overrides/_products_product.html.haml +++ b/app/views/admin/variant_overrides/_products_product.html.haml @@ -7,3 +7,4 @@ %td.on_demand{ ng: { show: 'columns.on_demand.visible' } } %td.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } } %td.inheritance{ ng: { show: 'columns.inheritance.visible' } } + %td.visibility{ ng: { show: 'columns.visibility.visible' } } diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index 87ec1709e4..1af6dab3d8 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -1,4 +1,4 @@ -%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants'}} +%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants | inventoryVariants:hub.id:showHidden'}} %td.producer{ ng: { show: 'columns.producer.visible' } } %td.product{ ng: { show: 'columns.product.visible' } } %span{ bo: { bind: 'variant.display_name || ""'} } @@ -17,3 +17,5 @@ %input{name: 'variant-overrides-{{ variant.id }}-default_stock', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].default_stock'}, placeholder: '{{ variant.default_stock ? variant.default_stock : "Default stock"}}', 'ofn-track-variant-override' => 'default_stock'} %td.inheritance{ ng: { show: 'columns.inheritance.visible' } } %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-inherit', ng: { model: 'inherit' }, 'track-inheritance' => true } + %td.visibility{ ng: { show: 'columns.visibility.visible' } } + %input.fullwidth{ :type => 'button', ng: { value: "inventoryItems[hub.id][variant.id].visible ? 'Hide' : 'Show'", click: "setVisibility(hub.id,variant.id,!inventoryItems[hub.id][variant.id].visible)", class: "{ hidden: !inventoryItems[hub.id][variant.id].visible}" } } diff --git a/app/views/admin/variant_overrides/index.html.haml b/app/views/admin/variant_overrides/index.html.haml index 9027445e62..ba5ce24479 100644 --- a/app/views/admin/variant_overrides/index.html.haml +++ b/app/views/admin/variant_overrides/index.html.haml @@ -1,12 +1,14 @@ = render 'admin/variant_overrides/header' = render 'admin/variant_overrides/data' -%div{ ng: { app: 'admin.variantOverrides', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } } +.margin-bottom-50{ ng: { app: 'admin.variantOverrides', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } } = render 'admin/variant_overrides/filters' + = render 'admin/variant_overrides/new_variants' %hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'hub' } } .controls.sixteen.columns.alpha.omega{ ng: { show: 'hub' } } %input.four.columns.alpha{ type: 'button', value: 'Reset Stock to Defaults', 'ng-click' => 'resetStock()' } - %div.nine.columns.alpha   + %input.four.columns{ type: 'button', ng: { value: "showHidden ? 'Hide Hidden' : 'Show Hidden'", click: 'showHidden = !showHidden' } } + %div.five.columns   = render 'admin/shared/columns_dropdown' %form{ name: 'variant_overrides_form' } diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 46e5caf191..003c1ab5f8 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -92,7 +92,7 @@ %div{ :class => "sixteen columns alpha", 'ng-show' => '!RequestMonitor.loading && filteredLineItems.length == 0'} %h1#no_results No orders found. - %div{ 'ng-hide' => 'RequestMonitor.loading || filteredLineItems.length == 0' } + .margin-bottom-50{ 'ng-hide' => 'RequestMonitor.loading || filteredLineItems.length == 0' } %form{ name: 'bulk_order_form' } %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } %thead diff --git a/config/routes.rb b/config/routes.rb index 62c4153d14..64dbc7c7f8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -109,6 +109,8 @@ Openfoodnetwork::Application.routes.draw do post :bulk_reset, on: :collection end + resources :inventory_items, only: [:create, :update] + resources :customers, only: [:index, :update] resource :content diff --git a/spec/controllers/admin/inventory_items_controller_spec.rb b/spec/controllers/admin/inventory_items_controller_spec.rb new file mode 100644 index 0000000000..4828f5c662 --- /dev/null +++ b/spec/controllers/admin/inventory_items_controller_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe Admin::InventoryItemsController, type: :controller do + # include AuthenticationWorkflow + + describe "create" do + context "json" do + let(:format) { :json } + + let(:enterprise) { create(:distributor_enterprise) } + let(:variant) { create(:variant) } + let(:inventory_item) { create(:inventory_item, enterprise: enterprise, variant: variant, visible: true) } + let(:params) { { format: format, inventory_item: { enterprise_id: enterprise.id, variant_id: variant.id, visible: false } } } + + context "where I don't manage the inventory item enterprise" do + before do + user = create(:user) + user.owned_enterprises << create(:enterprise) + allow(controller).to receive(:spree_current_user) { user } + end + + it "redirects to unauthorized" do + spree_post :create, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "where I manage the variant override hub" do + before do + allow(controller).to receive(:spree_current_user) { enterprise.owner } + end + + context "but the producer has not granted VO permission" do + it "redirects to unauthorized" do + spree_post :create, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "and the producer has granted VO permission" do + before do + create(:enterprise_relationship, parent: variant.product.supplier, child: enterprise, permissions_list: [:create_variant_overrides]) + end + + context "with acceptable data" do + it "allows me to create the inventory item" do + expect{ spree_post :create, params }.to change{InventoryItem.count}.by(1) + inventory_item = InventoryItem.last + expect(inventory_item.enterprise).to eq enterprise + expect(inventory_item.variant).to eq variant + expect(inventory_item.visible).to be false + end + end + + context "with unacceptable data" do + render_views + let!(:bad_params) { { format: format, inventory_item: { enterprise_id: enterprise.id, variant_id: variant.id, visible: nil } } } + + it "returns an error message" do + expect{ spree_post :create, bad_params }.to change{InventoryItem.count}.by(0) + expect(response.body).to eq Hash[:errors, ["Visible must be true or false"]].to_json + end + end + end + end + end + end + + describe "update" do + context "json" do + let(:format) { :json } + + let(:enterprise) { create(:distributor_enterprise) } + let(:variant) { create(:variant) } + let(:inventory_item) { create(:inventory_item, enterprise: enterprise, variant: variant, visible: true) } + let(:params) { { format: format, id: inventory_item.id, inventory_item: { visible: false } } } + + context "where I don't manage the inventory item enterprise" do + before do + user = create(:user) + user.owned_enterprises << create(:enterprise) + allow(controller).to receive(:spree_current_user) { user } + end + + it "redirects to unauthorized" do + spree_put :update, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "where I manage the variant override hub" do + before do + allow(controller).to receive(:spree_current_user) { enterprise.owner } + end + + context "but the producer has not granted VO permission" do + it "redirects to unauthorized" do + spree_put :update, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "and the producer has granted VO permission" do + before do + create(:enterprise_relationship, parent: variant.product.supplier, child: enterprise, permissions_list: [:create_variant_overrides]) + end + + context "with acceptable data" do + it "allows me to update the inventory item" do + spree_put :update, params + inventory_item.reload + expect(inventory_item.visible).to eq false + end + end + + context "with unacceptable data" do + render_views + let!(:bad_params) { { format: format, id: inventory_item.id, inventory_item: { visible: nil } } } + + it "returns an error message" do + expect{ spree_put :update, bad_params }.to change{InventoryItem.count}.by(0) + expect(response.body).to eq Hash[:errors, ["Visible must be true or false"]].to_json + end + end + end + end + end + end +end diff --git a/spec/controllers/admin/variant_overrides_controller_spec.rb b/spec/controllers/admin/variant_overrides_controller_spec.rb index d796f2d52f..3bd2632979 100644 --- a/spec/controllers/admin/variant_overrides_controller_spec.rb +++ b/spec/controllers/admin/variant_overrides_controller_spec.rb @@ -9,6 +9,7 @@ describe Admin::VariantOverridesController, type: :controller do let(:hub) { create(:distributor_enterprise) } let(:variant) { create(:variant) } + let!(:inventory_item) { create(:inventory_item, enterprise: hub, variant: variant, visible: true) } let!(:variant_override) { create(:variant_override, hub: hub, variant: variant) } let(:variant_override_params) { [ { id: variant_override.id, price: 123.45, count_on_hand: 321, sku: "MySKU", on_demand: false } ] } @@ -42,6 +43,14 @@ describe Admin::VariantOverridesController, type: :controller do create(:enterprise_relationship, parent: variant.product.supplier, child: hub, permissions_list: [:create_variant_overrides]) end + it "loads data" do + spree_put :bulk_update, format: format, variant_overrides: variant_override_params + expect(assigns[:hubs]).to eq [hub] + expect(assigns[:producers]).to eq [variant.product.supplier] + expect(assigns[:hub_permissions]).to eq Hash[hub.id,[variant.product.supplier.id]] + expect(assigns[:inventory_items]).to eq [inventory_item] + end + it "allows me to update the variant override" do spree_put :bulk_update, format: format, variant_overrides: variant_override_params variant_override.reload @@ -106,6 +115,14 @@ describe Admin::VariantOverridesController, type: :controller do context "where the producer has granted create_variant_overrides permission to the hub" do let!(:er1) { create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:create_variant_overrides]) } + it "loads data" do + spree_put :bulk_reset, params + expect(assigns[:hubs]).to eq [hub] + expect(assigns[:producers]).to eq [producer] + expect(assigns[:hub_permissions]).to eq Hash[hub.id,[producer.id]] + expect(assigns[:inventory_items]).to eq [] + end + it "updates stock to default values where reset is enabled" do expect(variant_override1.reload.count_on_hand).to eq 5 # reset enabled expect(variant_override2.reload.count_on_hand).to eq 2 # reset disabled diff --git a/spec/factories.rb b/spec/factories.rb index dd8d02e7bf..21c6cecd15 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -98,6 +98,12 @@ FactoryGirl.define do resettable false end + factory :inventory_item, :class => InventoryItem do + enterprise + variant + visible true + end + factory :enterprise, :class => Enterprise do owner { FactoryGirl.create :user } sequence(:name) { |n| "Enterprise #{n}" } diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index d28a20d6f9..767274b274 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -159,6 +159,7 @@ feature %q{ first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "Weight/Volume").click first("div#columns-dropdown div.menu div.menu_item", text: "Price").click + first("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "price", with: "$50.00" fill_in "final_weight_volume", :with => 2000 @@ -177,6 +178,7 @@ feature %q{ visit '/admin/orders/bulk_management' first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "Price").click + first("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "price", with: "$#{format("%.2f",li1.price * 5)}" fill_in "quantity", :with => 6 @@ -190,6 +192,7 @@ feature %q{ visit '/admin/orders/bulk_management' first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "Weight/Volume").click + first("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "final_weight_volume", with: "#{li1.final_weight_volume.round}" fill_in "quantity", :with => 6 @@ -211,6 +214,7 @@ feature %q{ first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "Producer").click + first("div#columns-dropdown", :text => "COLUMNS").click expect(page).to_not have_selector "th", :text => "PRODUCER" expect(page).to have_selector "th", :text => "NAME" diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 378afa3626..fafcb348db 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -28,13 +28,15 @@ feature %q{ end end - context "when a hub is selected" do + context "when inventory_items exist for variants" 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!(:inventory_item) { create(:inventory_item, enterprise: hub, variant: variant ) } 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!(:inventory_item_related) { create(:inventory_item, enterprise: hub, variant: variant_related ) } let!(:er2) { create(:enterprise_relationship, parent: producer_related, child: hub, permissions_list: [:create_variant_overrides]) } @@ -47,85 +49,85 @@ feature %q{ variant.option_values.first.destroy end - context "with no overrides" do + context "when a hub is selected" do before do visit '/admin/variant_overrides' select2_select hub.name, from: 'hub_id' 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_input "variant-overrides-#{variant.id}-price", placeholder: '1.23' - page.should have_input "variant-overrides-#{variant.id}-count_on_hand", placeholder: '12' + context "with no overrides" 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, '', ''] + 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' + 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' - # filters the products to those the hub can override - page.should_not have_content producer_unrelated.name - page.should_not have_content product_unrelated.name + # filters the products to those the hub can override + page.should_not have_content producer_unrelated.name + page.should_not have_content product_unrelated.name - # Filters based on the producer select filter - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to have_selector "#v_#{variant_related.id}" - select2_select producer.name, from: 'producer_filter' - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to_not have_selector "#v_#{variant_related.id}" - select2_select 'All', from: 'producer_filter' + # Filters based on the producer select filter + expect(page).to have_selector "#v_#{variant.id}" + expect(page).to have_selector "#v_#{variant_related.id}" + select2_select producer.name, from: 'producer_filter' + expect(page).to have_selector "#v_#{variant.id}" + expect(page).to_not have_selector "#v_#{variant_related.id}" + select2_select 'All', from: 'producer_filter' - # Filters based on the quick search box - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to have_selector "#v_#{variant_related.id}" - fill_in 'query', with: product.name - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to_not have_selector "#v_#{variant_related.id}" - fill_in 'query', with: '' + # Filters based on the quick search box + expect(page).to have_selector "#v_#{variant.id}" + expect(page).to have_selector "#v_#{variant_related.id}" + fill_in 'query', with: product.name + expect(page).to have_selector "#v_#{variant.id}" + expect(page).to_not have_selector "#v_#{variant_related.id}" + fill_in 'query', with: '' - # Clears the filters - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to have_selector "#v_#{variant_related.id}" - select2_select producer.name, from: 'producer_filter' - fill_in 'query', with: product_related.name - expect(page).to_not have_selector "#v_#{variant.id}" - expect(page).to_not have_selector "#v_#{variant_related.id}" - click_button 'Clear All' - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to have_selector "#v_#{variant_related.id}" - end + # Clears the filters + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" + select2_select producer.name, from: 'producer_filter' + fill_in 'query', with: product_related.name + expect(page).to_not have_selector "tr#v_#{variant.id}" + expect(page).to_not have_selector "tr#v_#{variant_related.id}" + click_button 'Clear All' + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" - it "creates new overrides" do - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "SKU").click - first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click - first("div#columns-dropdown", :text => "COLUMNS").click + # Show/Hide products + first("div#columns-dropdown", :text => "COLUMNS").click + first("div#columns-dropdown div.menu div.menu_item", text: "Show/Hide").click + first("div#columns-dropdown", :text => "COLUMNS").click + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" + within "tr#v_#{variant.id}" do click_button 'Hide' end + expect(page).to_not have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" + click_button 'Show Hidden' + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" + within "tr#v_#{variant.id}" do click_button 'Show' end + within "tr#v_#{variant_related.id}" do click_button 'Hide' end + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" + click_button 'Hide Hidden' + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to_not have_selector "tr#v_#{variant_related.id}" + end - fill_in "variant-overrides-#{variant.id}-sku", with: 'NEWSKU' - fill_in "variant-overrides-#{variant.id}-price", with: '777.77' - fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '123' - check "variant-overrides-#{variant.id}-on_demand" - page.should have_content "Changes to one override remain unsaved." + it "creates new overrides" do + first("div#columns-dropdown", :text => "COLUMNS").click + first("div#columns-dropdown div.menu div.menu_item", text: "SKU").click + first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click + first("div#columns-dropdown", :text => "COLUMNS").click - 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.sku.should == "NEWSKU" - vo.price.should == 777.77 - vo.count_on_hand.should == 123 - vo.on_demand.should == true - 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}-sku", with: 'NEWSKU' fill_in "variant-overrides-#{variant.id}-price", with: '777.77' fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '123' + check "variant-overrides-#{variant.id}-on_demand" page.should have_content "Changes to one override remain unsaved." expect do @@ -133,137 +135,190 @@ feature %q{ 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' + vo = VariantOverride.last + vo.variant_id.should == variant.id + vo.hub_id.should == hub.id + vo.sku.should == "NEWSKU" + vo.price.should == 777.77 + vo.count_on_hand.should == 123 + vo.on_demand.should == true + 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' + 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_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." + + 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 + let!(:vo) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111, default_stock: 1000, resettable: true) } + let!(:vo_no_auth) { create(:variant_override, variant: variant, hub: hub3, price: 1, count_on_hand: 2) } + let!(:product2) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } + let!(:variant2) { create(:variant, product: product2, unit_value: 8, price: 1.00, on_hand: 12) } + let!(:inventory_item2) { create(:inventory_item, enterprise: hub, variant: variant2) } + let!(:vo_no_reset) { create(:variant_override, variant: variant2, hub: hub, price: 3.99, count_on_hand: 40, default_stock: 100, resettable: false) } + let!(:variant3) { create(:variant, product: product, unit_value: 2, price: 5.00, on_hand: 6) } + let!(:vo3) { create(:variant_override, variant: variant3, hub: hub, price: 6, count_on_hand: 7, sku: "SOMESKU", default_stock: 100, resettable: false) } + let!(:inventory_item3) { create(:inventory_item, enterprise: hub, variant: variant3) } + + before do + visit '/admin/variant_overrides' + select2_select hub.name, from: 'hub_id' + end + + it "product values are affected by overrides" do + 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." - # 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.reload vo.variant_id.should == variant.id vo.hub_id.should == hub.id - vo.price.should == 111.11 - vo.count_on_hand.should == 111 + vo.price.should == 22.22 + vo.count_on_hand.should == 8888 + end + + # Any new fields added to the VO model need to be added to this test + it "deletes overrides when values are cleared" do + first("div#columns-dropdown", :text => "COLUMNS").click + first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click + first("div#columns-dropdown div.menu div.menu_item", text: "Reset Stock Level").click + first("div#columns-dropdown", :text => "COLUMNS").click + + # Clearing values by 'inheriting' + first("div#columns-dropdown", :text => "COLUMNS").click + first("div#columns-dropdown div.menu div.menu_item", text: "Inheritance").click + first("div#columns-dropdown", :text => "COLUMNS").click + check "variant-overrides-#{variant3.id}-inherit" + + # Clearing values manually + fill_in "variant-overrides-#{variant.id}-price", with: '' + fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '' + fill_in "variant-overrides-#{variant.id}-default_stock", with: '' + page.uncheck "variant-overrides-#{variant.id}-resettable" + page.should have_content "Changes to 2 overrides remain unsaved." + + expect do + click_button 'Save Changes' + page.should have_content "Changes saved." + end.to change(VariantOverride, :count).by(-2) + + VariantOverride.where(id: vo.id).should be_empty + VariantOverride.where(id: vo3.id).should be_empty + end + + it "resets stock to defaults" do + click_button 'Reset Stock to Defaults' + page.should have_content 'Stocks reset to defaults.' + vo.reload + page.should have_input "variant-overrides-#{variant.id}-count_on_hand", with: '1000', placeholder: '12' + vo.count_on_hand.should == 1000 + end + + it "doesn't reset stock levels if the behaviour is disabled" do + click_button 'Reset Stock to Defaults' + vo_no_reset.reload + page.should have_input "variant-overrides-#{variant2.id}-count_on_hand", with: '40', placeholder: '12' + vo_no_reset.count_on_hand.should == 40 + end + + it "prompts to save changes before reset if any are pending" do + fill_in "variant-overrides-#{variant.id}-price", with: '200' + click_button 'Reset Stock to Defaults' + page.should have_content "Save changes first" 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' - 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_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." - - 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 + end - context "with overrides" do - let!(:vo) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111, default_stock: 1000, resettable: true) } - let!(:vo_no_auth) { create(:variant_override, variant: variant, hub: hub3, price: 1, count_on_hand: 2) } - let!(:product2) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } - let!(:variant2) { create(:variant, product: product2, unit_value: 8, price: 1.00, on_hand: 12) } - let!(:vo_no_reset) { create(:variant_override, variant: variant2, hub: hub, price: 3.99, count_on_hand: 40, default_stock: 100, resettable: false) } - let!(:variant3) { create(:variant, product: product, unit_value: 2, price: 5.00, on_hand: 6) } - let!(:vo3) { create(:variant_override, variant: variant3, hub: hub, price: 6, count_on_hand: 7, sku: "SOMESKU", default_stock: 100, resettable: false) } + describe "when inventory_items do not exist for variants" do + let!(:product) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } + let!(:variant1) { create(:variant, product: product, unit_value: 1, price: 1.23, on_hand: 12) } + let!(:variant2) { create(:variant, product: product, unit_value: 2, price: 4.56, on_hand: 3) } + context "when a hub is selected" do before do visit '/admin/variant_overrides' select2_select hub.name, from: 'hub_id' end - it "product values are affected by overrides" do - 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 "shows new variants, and allows them to be added or ignored" do + expect(page).to_not have_selector "table#variant-overrides tr#v_#{variant1.id}" + expect(page).to_not have_selector "table#variant-overrides tr#v_#{variant2.id}" - 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(page).to have_table_row ['PRODUCER', 'PRODUCT', 'VARIANT', 'ADD', 'IGNORE'] + expect(page).to have_selector "table#new-variants tr#nv_#{variant1.id}" + expect(page).to have_selector "table#new-variants tr#nv_#{variant2.id}" + within "table#new-variants tr#nv_#{variant1.id}" do click_button 'Add' end + within "table#new-variants tr#nv_#{variant2.id}" do click_button 'Ignore' end + expect(page).to_not have_selector "table#new-variants tr#nv_#{variant1.id}" + expect(page).to_not have_selector "table#new-variants tr#nv_#{variant2.id}" - 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 - - # Any new fields added to the VO model need to be added to this test - it "deletes overrides when values are cleared" do - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click - first("div#columns-dropdown div.menu div.menu_item", text: "Reset Stock Level").click - first("div#columns-dropdown", :text => "COLUMNS").click - - # Clearing values manually - fill_in "variant-overrides-#{variant.id}-price", with: '' - fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '' - fill_in "variant-overrides-#{variant.id}-default_stock", with: '' - page.uncheck "variant-overrides-#{variant.id}-resettable" - page.should have_content "Changes to one override remain unsaved." - - # Clearing values by 'inheriting' - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Inheritance").click - first("div#columns-dropdown", :text => "COLUMNS").click - page.check "variant-overrides-#{variant3.id}-inherit" - - expect do - click_button 'Save Changes' - page.should have_content "Changes saved." - end.to change(VariantOverride, :count).by(-2) - - VariantOverride.where(id: vo.id).should be_empty - VariantOverride.where(id: vo3.id).should be_empty - end - - it "resets stock to defaults" do - click_button 'Reset Stock to Defaults' - page.should have_content 'Stocks reset to defaults.' - vo.reload - page.should have_input "variant-overrides-#{variant.id}-count_on_hand", with: '1000', placeholder: '12' - vo.count_on_hand.should == 1000 - end - - it "doesn't reset stock levels if the behaviour is disabled" do - click_button 'Reset Stock to Defaults' - vo_no_reset.reload - page.should have_input "variant-overrides-#{variant2.id}-count_on_hand", with: '40', placeholder: '12' - vo_no_reset.count_on_hand.should == 40 - end - - it "prompts to save changes before reset if any are pending" do - fill_in "variant-overrides-#{variant.id}-price", with: '200' - click_button 'Reset Stock to Defaults' - page.should have_content "Save changes first" + expect(page).to have_selector "table#variant-overrides tr#v_#{variant1.id}" + expect(page).to_not have_selector "table#variant-overrides tr#v_#{variant2.id}" end end end 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 699e1bc4ab..75e6102863 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 @@ -9,6 +9,7 @@ describe "VariantOverridesCtrl", -> variantOverrides = {} DirtyVariantOverrides = null dirtyVariantOverrides = {} + inventoryItems = {} StatusMessage = null statusMessage = {} @@ -18,6 +19,7 @@ describe "VariantOverridesCtrl", -> $provide.value 'SpreeApiKey', 'API_KEY' $provide.value 'variantOverrides', variantOverrides $provide.value 'dirtyVariantOverrides', dirtyVariantOverrides + $provide.value 'inventoryItems', inventoryItems null inject ($controller, _VariantOverrides_, _DirtyVariantOverrides_, _StatusMessage_) -> diff --git a/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee b/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee new file mode 100644 index 0000000000..49ea827900 --- /dev/null +++ b/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee @@ -0,0 +1,73 @@ +describe "InventoryItems service", -> + InventoryItems = InventoryItemResource = inventoryItems = $httpBackend = null + inventoryItems = {} + + beforeEach -> + module 'admin.inventoryItems' + module ($provide) -> + $provide.value 'inventoryItems', inventoryItems + null + + this.addMatchers + toDeepEqual: (expected) -> + return angular.equals(this.actual, expected) + + inject ($q, _$httpBackend_, _InventoryItems_, _InventoryItemResource_) -> + InventoryItems = _InventoryItems_ + InventoryItemResource = _InventoryItemResource_ + $httpBackend = _$httpBackend_ + + + describe "#setVisiblity", -> + describe "on an inventory item that already exists", -> + existing = null + + beforeEach -> + existing = new InventoryItemResource({ id: 1, enterprise_id: 2, variant_id: 3, visible: true }) + InventoryItems.inventoryItems[2] = {} + InventoryItems.inventoryItems[2][3] = existing + + describe "success", -> + beforeEach -> + $httpBackend.expectPUT('/admin/inventory_items/1.json', { id: 1, enterprise_id: 2, variant_id: 3, visible: false } ) + .respond 200, { id: 1, enterprise_id: 2, variant_id: 3, visible: false } + InventoryItems.setVisibility(2,3,false) + + it "saves the new visible value AFTER the request responds successfully", -> + expect(InventoryItems.inventoryItems[2][3].visible).toBe true + $httpBackend.flush() + expect(InventoryItems.inventoryItems[2][3].visible).toBe false + + describe "failure", -> + beforeEach -> + $httpBackend.expectPUT('/admin/inventory_items/1.json',{ id: 1, enterprise_id: 2, variant_id: 3, visible: null }) + .respond 422, { errors: ["Visible must be true or false"] } + InventoryItems.setVisibility(2,3,null) + + it "store the errors in the errors object", -> + expect(InventoryItems.errors).toEqual {} + $httpBackend.flush() + expect(InventoryItems.errors[2][3]).toEqual ["Visible must be true or false"] + + describe "on an inventory item that does not exist", -> + describe "success", -> + beforeEach -> + $httpBackend.expectPOST('/admin/inventory_items.json', { enterprise_id: 5, variant_id: 6, visible: false } ) + .respond 200, { id: 1, enterprise_id: 2, variant_id: 3, visible: false } + InventoryItems.setVisibility(5,6,false) + + it "saves the new visible value AFTER the request responds successfully", -> + expect(InventoryItems.inventoryItems).toEqual {} + $httpBackend.flush() + expect(InventoryItems.inventoryItems[5][6].visible).toBe false + + describe "failure", -> + beforeEach -> + $httpBackend.expectPOST('/admin/inventory_items.json',{ enterprise_id: 5, variant_id: 6, visible: null }) + .respond 422, { errors: ["Visible must be true or false"] } + InventoryItems.setVisibility(5,6,null) + + it "store the errors in the errors object", -> + expect(InventoryItems.errors).toEqual {} + $httpBackend.flush() + expect(InventoryItems.errors[5][6]).toEqual ["Visible must be true or false"] From 7008d26f6877c3e8ffbd9669a1fa49eb6a73859e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 20 Jan 2016 18:36:28 +1100 Subject: [PATCH 32/54] WIP: Alerting user to presence of new variant for inventory, new variants can be filtered + limited --- .../utils/directives/alert_row.js.coffee | 15 ++++ .../templates/admin/alert_row.html.haml | 8 ++ .../admin/components/alert_row.css.scss | 20 +++++ .../admin/components/save_bar.sass | 4 - app/assets/stylesheets/admin/offets.css.scss | 7 ++ .../stylesheets/admin/typography.css.scss | 9 +++ .../admin/inventory_items_controller.rb | 4 +- .../variant_overrides/_new_variants.html.haml | 79 +++++++++++++------ .../variant_overrides/_products.html.haml | 6 +- .../admin/variant_overrides/index.html.haml | 19 ++--- 10 files changed, 128 insertions(+), 43 deletions(-) create mode 100644 app/assets/javascripts/admin/utils/directives/alert_row.js.coffee create mode 100644 app/assets/javascripts/templates/admin/alert_row.html.haml create mode 100644 app/assets/stylesheets/admin/components/alert_row.css.scss create mode 100644 app/assets/stylesheets/admin/offets.css.scss create mode 100644 app/assets/stylesheets/admin/typography.css.scss diff --git a/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee b/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee new file mode 100644 index 0000000000..1c9afdb5b6 --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee @@ -0,0 +1,15 @@ +angular.module("admin.utils").directive "alertRow", -> + restrict: "E" + replace: true + scope: + message: '@' + buttonText: '@?' + buttonAction: '&?' + close: "&?" + transclude: true + templateUrl: "admin/alert_row.html" + link: (scope, element, attrs) -> + scope.dismiss = -> + scope.close() if scope.close? + element.hide() + return false diff --git a/app/assets/javascripts/templates/admin/alert_row.html.haml b/app/assets/javascripts/templates/admin/alert_row.html.haml new file mode 100644 index 0000000000..ed1ce00849 --- /dev/null +++ b/app/assets/javascripts/templates/admin/alert_row.html.haml @@ -0,0 +1,8 @@ +.sixteen.columns.alpha.omega.alert-row + .fifteen.columns.pad.alpha + %span.text-big{ ng: { bind: 'message'} } +     + %input{ type: 'button', ng: { value: "buttonText", show: 'buttonText && buttonAction', click: "buttonAction()" } } + .one.column.omega.pad.text-center + %a.close{ href: "#", ng: { click: "dismiss()" } } + × diff --git a/app/assets/stylesheets/admin/components/alert_row.css.scss b/app/assets/stylesheets/admin/components/alert_row.css.scss new file mode 100644 index 0000000000..6ddf9e731b --- /dev/null +++ b/app/assets/stylesheets/admin/components/alert_row.css.scss @@ -0,0 +1,20 @@ +.alert-row{ + font-weight: bold; + background-color: #eff5fc; + + .column, .columns { + padding-top: 8px; + padding-bottom: 8px; + &.alpha { padding-left: 10px; } + &.omega { padding-right: 10px; } + } + + span { + line-height: 3rem; + } + + a.close { + line-height: 3rem; + font-size: 2.0rem; + } +} diff --git a/app/assets/stylesheets/admin/components/save_bar.sass b/app/assets/stylesheets/admin/components/save_bar.sass index 3fdeb47339..c6b1236490 100644 --- a/app/assets/stylesheets/admin/components/save_bar.sass +++ b/app/assets/stylesheets/admin/components/save_bar.sass @@ -7,7 +7,3 @@ color: #5498da h5 color: #5498da - -// Making space for save-bar -.margin-bottom-50 - margin-bottom: 50px diff --git a/app/assets/stylesheets/admin/offets.css.scss b/app/assets/stylesheets/admin/offets.css.scss new file mode 100644 index 0000000000..762b7469f6 --- /dev/null +++ b/app/assets/stylesheets/admin/offets.css.scss @@ -0,0 +1,7 @@ +.margin-bottom-20 { + margin-bottom: 20px; +} + +.margin-bottom-50 { + margin-bottom: 50px; +} diff --git a/app/assets/stylesheets/admin/typography.css.scss b/app/assets/stylesheets/admin/typography.css.scss new file mode 100644 index 0000000000..20148df3f1 --- /dev/null +++ b/app/assets/stylesheets/admin/typography.css.scss @@ -0,0 +1,9 @@ +.text-normal { + font-size: 1.0rem; + font-weight: 300; +} + +.text-big { + font-size: 1.2rem; + font-weight: 300; +} diff --git a/app/controllers/admin/inventory_items_controller.rb b/app/controllers/admin/inventory_items_controller.rb index cb649fe6f2..432b955134 100644 --- a/app/controllers/admin/inventory_items_controller.rb +++ b/app/controllers/admin/inventory_items_controller.rb @@ -4,12 +4,12 @@ module Admin respond_to :json respond_override update: { json: { - success: lambda { sleep 3; render_as_json @inventory_item }, + success: lambda { render_as_json @inventory_item }, failure: lambda { render json: { errors: @inventory_item.errors.full_messages }, status: :unprocessable_entity } } } respond_override create: { json: { - success: lambda { sleep 3; render_as_json @inventory_item }, + success: lambda { render_as_json @inventory_item }, failure: lambda { render json: { errors: @inventory_item.errors.full_messages }, status: :unprocessable_entity } } } diff --git a/app/views/admin/variant_overrides/_new_variants.html.haml b/app/views/admin/variant_overrides/_new_variants.html.haml index ed87bc3e86..7ec3e76d0c 100644 --- a/app/views/admin/variant_overrides/_new_variants.html.haml +++ b/app/views/admin/variant_overrides/_new_variants.html.haml @@ -1,25 +1,54 @@ -%hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'newProducts.length > 0' } } -%table#new-variants{ ng: { show: 'newProducts.length > 0' } } - %col.producer{ width: "20%" } - %col.product{ width: "20%" } - %col.variant{ width: "40%" } - %col.add{ width: "10%" } - %col.ignore{ width: "10%" } - %thead - %tr - %th.producer Producer - %th.product Product - %th.variant Variant - %th.add Add - %th.ignore Ignore - %tbody{ bindonce: true, ng: { repeat: 'product in newProducts = (products | hubPermissions:hubPermissions:hub.id | newInventoryProducts:hub.id)' } } - %tr{ id: "nv_{{variant.id}}", ng: { repeat: 'variant in product.variants | newInventoryVariants:hub.id'} } - %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } - %td.product{ bo: { bind: 'product.name'} } - %td.variant - %span{ bo: { bind: 'variant.display_name || ""'} } - .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } - %td.add - %input.fullwidth{ :type => 'button', value: "Add", ng: { click: "setVisibility(hub.id,variant.id,true)" } } - %td.ignore - %input.fullwidth{ :type => 'button', value: "Ignore", ng: { click: "setVisibility(hub.id,variant.id,false)" } } +%hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'newProducts.length > 0 && addingNewVariants != false' } } + +%alert-row{ message: "There are {{ newProducts.length }} new products available to add to your inventory.", + button: { text: 'Review Now', action: "addingNewVariants = true" }, + close: "addingNewVariants = false", + ng: { show: 'newProducts.length > 0 && addingNewVariants == undefined' } } + +%div{ ng: { show: 'newProducts.length > 0 && addingNewVariants' } } + .sixteen.columns.alpha.omega.text-center.margin-bottom-20 + .one-third.column.alpha + %button.text-normal.fullwidth{ type: 'button', ng: { click: 'addingNewVariants = false' } } + %i.icon-chevron-left + Back to my inventory + -# .one-third.column + -# %button.text-big.fullwidth{ type: 'button', ng: { click: 'setVisibility(hub.id,variantIDs(filteredNewProducts),true)' } } + -# %i.icon-plus + -# Add All ({{filteredNewProducts.length}}) + -# .one-third.column.omega + -# %button.text-big.fullwidth{ type: 'button', ng: { click: 'setVisibility(hub.id,variantIDs(filteredNewProducts),false)' } } + -# %i.icon-remove + -# Ignore All ({{filteredNewProducts.length}}) + + %h2#no_results{ ng: { show: 'filteredNewProducts.length == 0' } } + No new products match the filters provided. + + %table#new-variants{ ng: { show: 'filteredNewProducts.length > 0' } } + %col.producer{ width: "20%" } + %col.product{ width: "20%" } + %col.variant{ width: "40%" } + %col.add{ width: "10%" } + %col.ignore{ width: "10%" } + %thead + %tr + %th.producer Producer + %th.product Product + %th.variant Variant + %th.add Add + %th.ignore Ignore + %tbody{ bindonce: true, ng: { repeat: 'product in filteredNewProducts = (newProducts = (products | hubPermissions:hubPermissions:hub.id | newInventoryProducts:hub.id) | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } + %tr{ id: "nv_{{variant.id}}", ng: { repeat: 'variant in product.variants | newInventoryVariants:hub.id'} } + %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } + %td.product{ bo: { bind: 'product.name'} } + %td.variant + %span{ bo: { bind: 'variant.display_name || ""'} } + .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } + %td.add + %input.fullwidth{ :type => 'button', value: "Add", ng: { click: "setVisibility(hub.id,variant.id,true)" } } + %td.ignore + %input.fullwidth{ :type => 'button', value: "Ignore", ng: { click: "setVisibility(hub.id,variant.id,false)" } } + + .sixteen.columns.alpha.omega.text-center{ ng: {show: 'productLimit < filteredNewProducts.length'}} + %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } + or + %input{ type: 'button', value: "Show All ({{ filteredNewProducts.length - productLimit }} More)", ng: { click: 'productLimit = filteredNewProducts.length' } } diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index e34f6eccaa..81fde749b3 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -1,4 +1,4 @@ -%table.index.bulk#variant-overrides{ ng: {show: 'hub'}} +%table.index.bulk#variant-overrides %col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } } %col.product{ width: "20%", ng: { show: 'columns.product.visible' } } %col.sku{ width: "20%", ng: { show: 'columns.sku.visible' } } @@ -24,7 +24,7 @@ = render 'admin/variant_overrides/products_product' = render 'admin/variant_overrides/products_variants' -.sixteen.columns.alpha.omega.text-center{ ng: {show: 'hub && productLimit < filteredProducts.length'}} +.sixteen.columns.alpha.omega.text-center{ ng: {show: 'productLimit < filteredProducts.length'}} %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } or - %input{ type: 'button', value: "Show All ({{ filteredProducts.length - productLimit }})", ng: { click: 'productLimit = filteredProducts.length' } } + %input{ type: 'button', value: "Show All ({{ filteredProducts.length - productLimit }} More)", ng: { click: 'productLimit = filteredProducts.length' } } diff --git a/app/views/admin/variant_overrides/index.html.haml b/app/views/admin/variant_overrides/index.html.haml index ba5ce24479..4fa04bfec9 100644 --- a/app/views/admin/variant_overrides/index.html.haml +++ b/app/views/admin/variant_overrides/index.html.haml @@ -4,13 +4,14 @@ .margin-bottom-50{ ng: { app: 'admin.variantOverrides', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } } = render 'admin/variant_overrides/filters' = render 'admin/variant_overrides/new_variants' - %hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'hub' } } - .controls.sixteen.columns.alpha.omega{ ng: { show: 'hub' } } - %input.four.columns.alpha{ type: 'button', value: 'Reset Stock to Defaults', 'ng-click' => 'resetStock()' } - %input.four.columns{ type: 'button', ng: { value: "showHidden ? 'Hide Hidden' : 'Show Hidden'", click: 'showHidden = !showHidden' } } - %div.five.columns   - = render 'admin/shared/columns_dropdown' + %div{ ng: { show: 'hub', hide: 'addingNewVariants' } } + %hr.divider.sixteen.columns.alpha.omega + .controls.sixteen.columns.alpha.omega + %input.four.columns.alpha{ type: 'button', value: 'Reset Stock to Defaults', 'ng-click' => 'resetStock()' } + %input.four.columns{ type: 'button', ng: { value: "showHidden ? 'Hide Hidden' : 'Show Hidden'", click: 'showHidden = !showHidden' } } + %div.five.columns   + = render 'admin/shared/columns_dropdown' - %form{ name: 'variant_overrides_form' } - %save-bar{ save: "update()", form: "variant_overrides_form" } - = render 'admin/variant_overrides/products' + %form{ name: 'variant_overrides_form' } + %save-bar{ save: "update()", form: "variant_overrides_form" } + = render 'admin/variant_overrides/products' From 8f37aa0522032827f901970d72aada1bb05d6ccc Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 24 Jan 2016 09:57:39 +1100 Subject: [PATCH 33/54] WIP: Adding Loading flash to variant overrides page --- .../services/data_fetcher.js.coffee | 19 ++++++++----------- .../variant_overrides_controller.js.coffee | 3 ++- .../admin/openfoodnetwork.css.scss | 5 +++++ .../shared/_bulk_actions_dropdown.html.haml | 2 +- .../variant_overrides/_new_variants.html.haml | 8 +++++--- .../admin/variant_overrides/index.html.haml | 11 ++++++++++- 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/admin/index_utils/services/data_fetcher.js.coffee b/app/assets/javascripts/admin/index_utils/services/data_fetcher.js.coffee index bf5580a3b2..edfb2c0a06 100644 --- a/app/assets/javascripts/admin/index_utils/services/data_fetcher.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/data_fetcher.js.coffee @@ -1,12 +1,9 @@ -angular.module("admin.indexUtils").factory "dataFetcher", [ - "$http", "$q" - ($http, $q) -> - return (dataLocation) -> - deferred = $q.defer() - $http.get(dataLocation).success((data) -> - deferred.resolve data - ).error -> - deferred.reject() +angular.module("admin.indexUtils").factory "dataFetcher", ($http, $q, RequestMonitor) -> + return (dataLocation) -> + deferred = $q.defer() + RequestMonitor.load $http.get(dataLocation).success((data) -> + deferred.resolve data + ).error -> + deferred.reject() - deferred.promise -] + deferred.promise diff --git a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee index c7743efa4f..8cd60fefb9 100644 --- a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, InventoryItems, VariantOverrides, DirtyVariantOverrides) -> +angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, SpreeApiAuth, PagedFetcher, StatusMessage, RequestMonitor, hubs, producers, hubPermissions, InventoryItems, VariantOverrides, DirtyVariantOverrides) -> $scope.hubs = Indexer.index hubs $scope.hub = null $scope.products = [] @@ -11,6 +11,7 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", $scope.inventoryItems = InventoryItems.inventoryItems $scope.setVisibility = InventoryItems.setVisibility $scope.StatusMessage = StatusMessage + $scope.RequestMonitor = RequestMonitor $scope.columns = Columns.setColumns producer: { name: "Producer", visible: true } diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index e0916b4e79..17d43ff3a5 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -166,11 +166,16 @@ table#listing_enterprise_groups { } } +// TODO: remove this, use class below #no_results { font-weight:bold; color: #DA5354; } +.no-results { + font-weight:bold; + color: #DA5354; +} #loading { text-align: center; diff --git a/app/views/admin/shared/_bulk_actions_dropdown.html.haml b/app/views/admin/shared/_bulk_actions_dropdown.html.haml index 912fe6662a..c1a7f3167d 100644 --- a/app/views/admin/shared/_bulk_actions_dropdown.html.haml +++ b/app/views/admin/shared/_bulk_actions_dropdown.html.haml @@ -4,4 +4,4 @@ %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } %div.menu{ 'ng-show' => "expanded" } .three.columns.alpha.menu_item{ 'ng-repeat' => "action in bulkActions", 'ng-click' => "$eval(action.callback)(filteredLineItems)", 'ofn-close-on-click' => true } - %span.three.columns.omega {{action.name }} + %span.three.columns.omega {{ action.name }} diff --git a/app/views/admin/variant_overrides/_new_variants.html.haml b/app/views/admin/variant_overrides/_new_variants.html.haml index 7ec3e76d0c..fe75a29cd8 100644 --- a/app/views/admin/variant_overrides/_new_variants.html.haml +++ b/app/views/admin/variant_overrides/_new_variants.html.haml @@ -6,9 +6,11 @@ ng: { show: 'newProducts.length > 0 && addingNewVariants == undefined' } } %div{ ng: { show: 'newProducts.length > 0 && addingNewVariants' } } - .sixteen.columns.alpha.omega.text-center.margin-bottom-20 - .one-third.column.alpha - %button.text-normal.fullwidth{ type: 'button', ng: { click: 'addingNewVariants = false' } } + .sixteen.columns.alpha.omega.margin-bottom-20 + .two-thirds.column.alpha.text-normal + Add products to your inventory by clicking 'Add', or hide them from view by clicking 'Ignore'. Don't worry, you can always change your mind later. + .one-third.column.omega + %button.fullwidth{ type: 'button', ng: { click: 'addingNewVariants = false' } } %i.icon-chevron-left Back to my inventory -# .one-third.column diff --git a/app/views/admin/variant_overrides/index.html.haml b/app/views/admin/variant_overrides/index.html.haml index 4fa04bfec9..5c7a250cb9 100644 --- a/app/views/admin/variant_overrides/index.html.haml +++ b/app/views/admin/variant_overrides/index.html.haml @@ -3,8 +3,16 @@ .margin-bottom-50{ ng: { app: 'admin.variantOverrides', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } } = render 'admin/variant_overrides/filters' + %div.sixteen.columns.alpha#loading{ ng: { cloak: true, if: 'hub && products.length == 0 && RequestMonitor.loading' } } + %img.spinner{ src: "/assets/spinning-circles.svg" } + %h1 LOADING INVENTORY = render 'admin/variant_overrides/new_variants' - %div{ ng: { show: 'hub', hide: 'addingNewVariants' } } + %span.text-big.no-results{ ng: { show: 'hub && !addingNewVariants && products.length > 0 && filteredProducts.length == 0' } } + No products matching products found. + %span.text-big.no-results{ ng: { show: 'hub && !addingNewVariants && products.length == 0 && !RequestMonitor.loading' } } + There are no products in {{ hub.name }}'s inventory + %button{ value: 'Add Some!' } + %div{ ng: { cloak: true, show: 'hub && !addingNewVariants && filteredProducts.length > 0' } } %hr.divider.sixteen.columns.alpha.omega .controls.sixteen.columns.alpha.omega %input.four.columns.alpha{ type: 'button', value: 'Reset Stock to Defaults', 'ng-click' => 'resetStock()' } @@ -12,6 +20,7 @@ %div.five.columns   = render 'admin/shared/columns_dropdown' + %form{ name: 'variant_overrides_form' } %save-bar{ save: "update()", form: "variant_overrides_form" } = render 'admin/variant_overrides/products' From 28b143da732c2ad1f4e531c5d563f1a680d57ce3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Mon, 1 Feb 2016 15:30:08 +1100 Subject: [PATCH 34/54] WIP: Splitting Variant overrides into views --- .../controllers/dropdown_controller.js.coffee | 2 - .../directives/close_on_click.js.coffee | 2 +- .../dropdown/directives/dropdown.js.coffee | 3 + .../directives/toggle_column.js.coffee | 2 +- .../directives/toggle_view.js.coffee | 11 ++++ .../index_utils/services/views.js.coffee | 16 ++++++ .../utils/directives/alert_row.js.coffee | 5 +- .../variant_overrides_controller.js.coffee | 17 +++++- .../inventory_products_filter.js.coffee | 15 +++-- .../inventory_variants_filter.js.coffee | 10 ++-- .../new_inventory_products_filter.js.coffee | 1 + .../new_inventory_variants_filter.js.coffee | 7 --- .../templates/admin/alert_row.html.haml | 4 +- .../admin/components/alert_row.css.scss | 1 + .../stylesheets/admin/dropdown.css.scss | 28 ++++++++- .../{offets.css.scss => offsets.css.scss} | 0 .../admin/variant_overrides.css.sass | 3 + .../shared/_bulk_actions_dropdown.html.haml | 13 ++--- .../admin/shared/_columns_dropdown.html.haml | 15 +++-- .../admin/shared/_views_dropdown.html.haml | 7 +++ .../variant_overrides/_controls.html.haml | 15 +++++ .../_hidden_products.html.haml | 22 +++++++ .../_loading_flash.html.haml | 3 + .../variant_overrides/_new_products.html.haml | 26 +++++++++ .../_new_products_alert.html.haml | 5 ++ .../variant_overrides/_new_variants.html.haml | 56 ------------------ .../variant_overrides/_no_results.html.haml | 7 +++ .../variant_overrides/_products.html.haml | 57 +++++++++---------- .../_products_variants.html.haml | 5 +- .../variant_overrides/_show_more.html.haml | 4 ++ .../admin/variant_overrides/index.html.haml | 30 +++------- spec/features/admin/variant_overrides_spec.rb | 54 +++++++++++------- .../index_utils/services/views_spec.js.coffee | 37 ++++++++++++ 33 files changed, 311 insertions(+), 172 deletions(-) delete mode 100644 app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee create mode 100644 app/assets/javascripts/admin/index_utils/directives/toggle_view.js.coffee create mode 100644 app/assets/javascripts/admin/index_utils/services/views.js.coffee delete mode 100644 app/assets/javascripts/admin/variant_overrides/filters/new_inventory_variants_filter.js.coffee rename app/assets/stylesheets/admin/{offets.css.scss => offsets.css.scss} (100%) create mode 100644 app/views/admin/shared/_views_dropdown.html.haml create mode 100644 app/views/admin/variant_overrides/_controls.html.haml create mode 100644 app/views/admin/variant_overrides/_hidden_products.html.haml create mode 100644 app/views/admin/variant_overrides/_loading_flash.html.haml create mode 100644 app/views/admin/variant_overrides/_new_products.html.haml create mode 100644 app/views/admin/variant_overrides/_new_products_alert.html.haml delete mode 100644 app/views/admin/variant_overrides/_new_variants.html.haml create mode 100644 app/views/admin/variant_overrides/_no_results.html.haml create mode 100644 app/views/admin/variant_overrides/_show_more.html.haml create mode 100644 spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee diff --git a/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee b/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee deleted file mode 100644 index 02e47ff9f7..0000000000 --- a/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -angular.module("admin.dropdown").controller "DropDownCtrl", ($scope) -> - $scope.expanded = false diff --git a/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee b/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee index 9b506cb8fb..1ab9e5c8a4 100644 --- a/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee +++ b/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee @@ -1,4 +1,4 @@ - angular.module("admin.dropdown").directive "ofnCloseOnClick", ($document) -> + angular.module("admin.dropdown").directive "closeOnClick", () -> link: (scope, element, attrs) -> element.click (event) -> event.stopPropagation() diff --git a/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee b/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee index b4ca2869d7..f26894afbc 100644 --- a/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee +++ b/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee @@ -1,6 +1,9 @@ angular.module("admin.dropdown").directive "ofnDropDown", ($document) -> restrict: 'C' + scope: true link: (scope, element, attrs) -> + scope.expanded = false + outsideClickListener = (event) -> unless $(event.target).is("div.ofn-drop-down##{attrs.id} div.menu") || $(event.target).parents("div.ofn-drop-down##{attrs.id} div.menu").length > 0 diff --git a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee index 2910e9a7a1..614b8d9346 100644 --- a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.indexUtils").directive "ofnToggleColumn", (Columns) -> +angular.module("admin.indexUtils").directive "toggleColumn", (Columns) -> link: (scope, element, attrs) -> element.addClass "selected" if scope.column.visible diff --git a/app/assets/javascripts/admin/index_utils/directives/toggle_view.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_view.js.coffee new file mode 100644 index 0000000000..5741c06fc7 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/toggle_view.js.coffee @@ -0,0 +1,11 @@ +angular.module("admin.indexUtils").directive "toggleView", (Views) -> + link: (scope, element, attrs) -> + Views.register + element.addClass "selected" if scope.view.visible + + element.click "click", -> + scope.$apply -> + Views.selectView(scope.viewKey) + + scope.$watch "view.visible", (newValue, oldValue) -> + element.toggleClass "selected", scope.view.visible diff --git a/app/assets/javascripts/admin/index_utils/services/views.js.coffee b/app/assets/javascripts/admin/index_utils/services/views.js.coffee new file mode 100644 index 0000000000..cf26a73ad8 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/views.js.coffee @@ -0,0 +1,16 @@ +angular.module("admin.indexUtils").factory 'Views', ($rootScope) -> + new class Views + views: {} + currentView: null + + setViews: (views) => + @views = {} + for key, view of views + @views[key] = view + @selectView(key) if view.visible + @views + + selectView: (selectedKey) => + @currentView = @views[selectedKey] + for key, view of @views + view.visible = (key == selectedKey) diff --git a/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee b/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee index 1c9afdb5b6..c7bd2840a1 100644 --- a/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee @@ -5,11 +5,14 @@ angular.module("admin.utils").directive "alertRow", -> message: '@' buttonText: '@?' buttonAction: '&?' + dismissed: '=?' close: "&?" transclude: true templateUrl: "admin/alert_row.html" link: (scope, element, attrs) -> + scope.dismissed = false + scope.dismiss = -> + scope.dismissed = true scope.close() if scope.close? - element.hide() return false diff --git a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee index 8cd60fefb9..b754c9b722 100644 --- a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee @@ -1,17 +1,23 @@ -angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, SpreeApiAuth, PagedFetcher, StatusMessage, RequestMonitor, hubs, producers, hubPermissions, InventoryItems, VariantOverrides, DirtyVariantOverrides) -> +angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, Views, SpreeApiAuth, PagedFetcher, StatusMessage, RequestMonitor, hubs, producers, hubPermissions, InventoryItems, VariantOverrides, DirtyVariantOverrides) -> $scope.hubs = Indexer.index hubs $scope.hub = null $scope.products = [] $scope.producers = producers $scope.producersByID = Indexer.index producers $scope.hubPermissions = hubPermissions - $scope.showHidden = false $scope.productLimit = 10 $scope.variantOverrides = VariantOverrides.variantOverrides $scope.inventoryItems = InventoryItems.inventoryItems $scope.setVisibility = InventoryItems.setVisibility $scope.StatusMessage = StatusMessage $scope.RequestMonitor = RequestMonitor + $scope.selectView = Views.selectView + $scope.currentView = -> Views.currentView + + $scope.views = Views.setViews + inventory: { name: "Inventory Products", visible: true } + hidden: { name: "Hidden Products", visible: false } + new: { name: "New Products", visible: false } $scope.columns = Columns.setColumns producer: { name: "Producer", visible: true } @@ -22,7 +28,9 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", on_demand: { name: "On Demand", visible: false } reset: { name: "Reset Stock Level", visible: false } inheritance: { name: "Inheritance", visible: false } - visibility: { name: "Show/Hide", visible: false } + visibility: { name: "Hide", visible: false } + + $scope.bulkActions = [ name: "Reset Stock Levels To Defaults", callback: 'resetStock' ] $scope.resetSelectFilters = -> $scope.producerFilter = 0 @@ -30,6 +38,9 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", $scope.resetSelectFilters() + $scope.filtersApplied = -> + $scope.producerFilter != 0 || $scope.query != '' + $scope.initialise = -> SpreeApiAuth.authorise() .then -> diff --git a/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee index 4f5a3c5191..6025e43826 100644 --- a/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee @@ -1,12 +1,17 @@ angular.module("admin.variantOverrides").filter "inventoryProducts", ($filter, InventoryItems) -> - return (products, hub_id, showHidden) -> + return (products, hub_id, views) -> return [] if !hub_id return $filter('filter')(products, (product) -> for variant in product.variants if InventoryItems.inventoryItems.hasOwnProperty(hub_id) && InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) - if showHidden - return true + if InventoryItems.inventoryItems[hub_id][variant.id].visible + # Important to only return if true, as other variants for this product might be visible + return true if views.inventory.visible else - return true if InventoryItems.inventoryItems[hub_id][variant.id].visible - false + # Important to only return if true, as other variants for this product might be visible + return true if views.hidden.visible + else + # Important to only return if true, as other variants for this product might be visible + return true if views.new.visible + false , true) diff --git a/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee index 51238fa189..81ddc8215f 100644 --- a/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee @@ -1,12 +1,12 @@ angular.module("admin.variantOverrides").filter "inventoryVariants", ($filter, InventoryItems) -> - return (variants, hub_id, showHidden) -> + return (variants, hub_id, views) -> return [] if !hub_id return $filter('filter')(variants, (variant) -> if InventoryItems.inventoryItems.hasOwnProperty(hub_id) && InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) - if showHidden - return true + if InventoryItems.inventoryItems[hub_id][variant.id].visible + return views.inventory.visible else - return InventoryItems.inventoryItems[hub_id][variant.id].visible + return views.hidden.visible else - false + return views.new.visible , true) diff --git a/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee index ec1148c22e..fcc6f395d8 100644 --- a/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee @@ -5,4 +5,5 @@ angular.module("admin.variantOverrides").filter "newInventoryProducts", ($filter return $filter('filter')(products, (product) -> for variant in product.variants return true if !InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) + false , true) diff --git a/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_variants_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_variants_filter.js.coffee deleted file mode 100644 index d06724a704..0000000000 --- a/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_variants_filter.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -angular.module("admin.variantOverrides").filter "newInventoryVariants", ($filter, InventoryItems) -> - return (variants, hub_id) -> - return [] if !hub_id - return variants unless InventoryItems.inventoryItems.hasOwnProperty(hub_id) - return $filter('filter')(variants, (variant) -> - !InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) - , true) diff --git a/app/assets/javascripts/templates/admin/alert_row.html.haml b/app/assets/javascripts/templates/admin/alert_row.html.haml index ed1ce00849..c1dfe45c6f 100644 --- a/app/assets/javascripts/templates/admin/alert_row.html.haml +++ b/app/assets/javascripts/templates/admin/alert_row.html.haml @@ -1,6 +1,6 @@ -.sixteen.columns.alpha.omega.alert-row +.sixteen.columns.alpha.omega.alert-row{ ng: { show: '!dismissed' } } .fifteen.columns.pad.alpha - %span.text-big{ ng: { bind: 'message'} } + %span.message.text-big{ ng: { bind: 'message'} }     %input{ type: 'button', ng: { value: "buttonText", show: 'buttonText && buttonAction', click: "buttonAction()" } } .one.column.omega.pad.text-center diff --git a/app/assets/stylesheets/admin/components/alert_row.css.scss b/app/assets/stylesheets/admin/components/alert_row.css.scss index 6ddf9e731b..4c74afc56f 100644 --- a/app/assets/stylesheets/admin/components/alert_row.css.scss +++ b/app/assets/stylesheets/admin/components/alert_row.css.scss @@ -1,4 +1,5 @@ .alert-row{ + margin-bottom: 10px; font-weight: bold; background-color: #eff5fc; diff --git a/app/assets/stylesheets/admin/dropdown.css.scss b/app/assets/stylesheets/admin/dropdown.css.scss index 2dfa369c87..b848bc3c2c 100644 --- a/app/assets/stylesheets/admin/dropdown.css.scss +++ b/app/assets/stylesheets/admin/dropdown.css.scss @@ -27,9 +27,12 @@ -ms-user-select: none; user-select: none; text-align: center; + margin-right: 10px; &.right { float: right; + margin-right: 0px; + margin-left: 10px; } &:hover, &.expanded { @@ -55,12 +58,35 @@ background-color: #ffffff; box-shadow: 1px 3px 10px #888888; z-index: 100; + white-space: nowrap; + .menu_item { margin: 0px; - padding: 2px 0px; + padding: 2px 10px; color: #454545; text-align: left; + display: block; + + .check { + display: inline-block; + text-align: center; + width: 40px; + &:before { + content: "\00a0"; + } + } + + .name { + display: inline-block; + padding: 0px 15px 0px 0px; + } + + &.selected{ + .check:before { + content: "\2713"; + } + } } .menu_item:hover { diff --git a/app/assets/stylesheets/admin/offets.css.scss b/app/assets/stylesheets/admin/offsets.css.scss similarity index 100% rename from app/assets/stylesheets/admin/offets.css.scss rename to app/assets/stylesheets/admin/offsets.css.scss diff --git a/app/assets/stylesheets/admin/variant_overrides.css.sass b/app/assets/stylesheets/admin/variant_overrides.css.sass index c0f51658b8..0488c1d56b 100644 --- a/app/assets/stylesheets/admin/variant_overrides.css.sass +++ b/app/assets/stylesheets/admin/variant_overrides.css.sass @@ -1,3 +1,6 @@ .variant-override-unit float: right font-style: italic + +button.hide:hover + background-color: #DA5354 diff --git a/app/views/admin/shared/_bulk_actions_dropdown.html.haml b/app/views/admin/shared/_bulk_actions_dropdown.html.haml index c1a7f3167d..15959a6d15 100644 --- a/app/views/admin/shared/_bulk_actions_dropdown.html.haml +++ b/app/views/admin/shared/_bulk_actions_dropdown.html.haml @@ -1,7 +1,6 @@ -.three.columns - .ofn-drop-down#bulk-actions-dropdown{ 'ng-controller' => "DropDownCtrl" } - %span.icon-check   Actions - %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } - %div.menu{ 'ng-show' => "expanded" } - .three.columns.alpha.menu_item{ 'ng-repeat' => "action in bulkActions", 'ng-click' => "$eval(action.callback)(filteredLineItems)", 'ofn-close-on-click' => true } - %span.three.columns.omega {{ action.name }} +.ofn-drop-down#bulk-actions-dropdown + %span.icon-check   Actions + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + .menu{ 'ng-show' => "expanded" } + .menu_item{ 'ng-repeat' => "action in bulkActions", 'ng-click' => "$eval(action.callback)(filteredLineItems)", 'close-on-click' => true } + %span.name {{ action.name }} diff --git a/app/views/admin/shared/_columns_dropdown.html.haml b/app/views/admin/shared/_columns_dropdown.html.haml index b16d388c1d..ee8e552bb3 100644 --- a/app/views/admin/shared/_columns_dropdown.html.haml +++ b/app/views/admin/shared/_columns_dropdown.html.haml @@ -1,8 +1,7 @@ -%div.three.columns.omega - %div.ofn-drop-down.right#columns-dropdown{ 'ng-controller' => "DropDownCtrl" } - %span{ :class => 'icon-reorder' }   Columns - %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } - %div.menu{ 'ng-show' => "expanded" } - %div.menu_item.three.columns.alpha.omega{ 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true } - %span.one.column.alpha.text-center {{ column.visible && "✓" || !column.visible && " " }} - %span.two.columns.omega {{column.name }} +.ofn-drop-down.right#columns-dropdown + %span{ :class => 'icon-reorder' }   Columns + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + %div.menu{ 'ng-show' => "expanded" } + %div.menu_item{ ng: { repeat: "column in columns" }, toggle: { column: true } } + %span.check + %span.name {{column.name }} diff --git a/app/views/admin/shared/_views_dropdown.html.haml b/app/views/admin/shared/_views_dropdown.html.haml new file mode 100644 index 0000000000..addf43a1ce --- /dev/null +++ b/app/views/admin/shared/_views_dropdown.html.haml @@ -0,0 +1,7 @@ +.ofn-drop-down#views-dropdown + %span{ :class => 'icon-eye-open' }   Viewing: {{ currentView().name }} + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + %div.menu{ 'ng-show' => "expanded" } + %div.menu_item{ ng: { repeat: "(viewKey, view) in views" }, toggle: { view: true }, 'close-on-click' => true } + %span.check + %span.name {{ view.name }} diff --git a/app/views/admin/variant_overrides/_controls.html.haml b/app/views/admin/variant_overrides/_controls.html.haml new file mode 100644 index 0000000000..bb70f2cf5a --- /dev/null +++ b/app/views/admin/variant_overrides/_controls.html.haml @@ -0,0 +1,15 @@ +%hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'hub && products.length > 0' } } +.controls.sixteen.columns.alpha.omega{ ng: { show: 'hub && products.length > 0' } } + .eight.columns.alpha + = render 'admin/shared/bulk_actions_dropdown' + = render 'admin/shared/views_dropdown' + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.inventory.visible' } , data: { powertip: 'This is your inventory of products. Products must be listed here before you can sell them in your shop. To add products to your inventory, select \'New Products\' from the Viewing dropdown.' } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.hidden.visible' } , data: { powertip: 'These products have been hidden from your inventory and will not be available in your shop. You can click \'Add\' to add a product to you inventory.' } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.new.visible' } , data: { powertip: 'These products are available to be added to your inventory. Click \'Add\' to add a product to your inventory, or \'Hide\' to hide it from view. You can always change your mind later!' } } + .four.columns   + .four.columns.omega{ ng: { show: 'views.new.visible' } } + %button.fullwidth{ type: 'button', ng: { click: "selectView('inventory')" } } + %i.icon-chevron-left + Back to my inventory + .four.columns.omega{ng: { show: 'views.inventory.visible' } } + = render 'admin/shared/columns_dropdown' diff --git a/app/views/admin/variant_overrides/_hidden_products.html.haml b/app/views/admin/variant_overrides/_hidden_products.html.haml new file mode 100644 index 0000000000..1ef143afe0 --- /dev/null +++ b/app/views/admin/variant_overrides/_hidden_products.html.haml @@ -0,0 +1,22 @@ +%div{ ng: { show: 'views.hidden.visible' } } + %table#hidden-products{ ng: { show: 'filteredProducts.length > 0' } } + %col.producer{ width: "20%" } + %col.product{ width: "20%" } + %col.variant{ width: "30%" } + %col.add{ width: "15%" } + %thead + %tr + %th.producer Producer + %th.product Product + %th.variant Variant + %th.add Add + %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } + %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub.id:views' } } + %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } + %td.product{ bo: { bind: 'product.name'} } + %td.variant + %span{ bo: { bind: 'variant.display_name || ""'} } + .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } + %td.add + %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub.id,variant.id,true)" } } + = t(:add) diff --git a/app/views/admin/variant_overrides/_loading_flash.html.haml b/app/views/admin/variant_overrides/_loading_flash.html.haml new file mode 100644 index 0000000000..24cc7c041d --- /dev/null +++ b/app/views/admin/variant_overrides/_loading_flash.html.haml @@ -0,0 +1,3 @@ +%div.sixteen.columns.alpha.omega#loading{ ng: { cloak: true, if: 'hub && products.length == 0 && RequestMonitor.loading' } } + %img.spinner{ src: "/assets/spinning-circles.svg" } + %h1 LOADING INVENTORY diff --git a/app/views/admin/variant_overrides/_new_products.html.haml b/app/views/admin/variant_overrides/_new_products.html.haml new file mode 100644 index 0000000000..f4e519ce3f --- /dev/null +++ b/app/views/admin/variant_overrides/_new_products.html.haml @@ -0,0 +1,26 @@ +%table#new-products{ ng: { show: 'views.new.visible && filteredProducts.length > 0' } } + %col.producer{ width: "20%" } + %col.product{ width: "20%" } + %col.variant{ width: "30%" } + %col.add{ width: "15%" } + %col.hide{ width: "15%" } + %thead + %tr + %th.producer Producer + %th.product Product + %th.variant Variant + %th.add Add + %th.hide Hide + %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } + %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub.id:views' } } + %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } + %td.product{ bo: { bind: 'product.name'} } + %td.variant + %span{ bo: { bind: 'variant.display_name || ""'} } + .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } + %td.add + %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub.id,variant.id,true)" } } + = t(:add) + %td.hide + %button.fullwidth.hide.icon-remove{ ng: { click: "setVisibility(hub.id,variant.id,false)" } } + = t(:hide) diff --git a/app/views/admin/variant_overrides/_new_products_alert.html.haml b/app/views/admin/variant_overrides/_new_products_alert.html.haml new file mode 100644 index 0000000000..48b9d041c5 --- /dev/null +++ b/app/views/admin/variant_overrides/_new_products_alert.html.haml @@ -0,0 +1,5 @@ +%div{ ng: { show: '(newProductCount = (products | newInventoryProducts:hub_id).length) > 0 && !views.new.visible && !alertDismissed' } } + %hr.divider.sixteen.columns.alpha.omega + %alert-row{ message: "There are {{ newProductCount }} new products available to add to your inventory.", + dismissed: "alertDismissed", + button: { text: 'Review Now', action: "selectView('new')" } } diff --git a/app/views/admin/variant_overrides/_new_variants.html.haml b/app/views/admin/variant_overrides/_new_variants.html.haml deleted file mode 100644 index fe75a29cd8..0000000000 --- a/app/views/admin/variant_overrides/_new_variants.html.haml +++ /dev/null @@ -1,56 +0,0 @@ -%hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'newProducts.length > 0 && addingNewVariants != false' } } - -%alert-row{ message: "There are {{ newProducts.length }} new products available to add to your inventory.", - button: { text: 'Review Now', action: "addingNewVariants = true" }, - close: "addingNewVariants = false", - ng: { show: 'newProducts.length > 0 && addingNewVariants == undefined' } } - -%div{ ng: { show: 'newProducts.length > 0 && addingNewVariants' } } - .sixteen.columns.alpha.omega.margin-bottom-20 - .two-thirds.column.alpha.text-normal - Add products to your inventory by clicking 'Add', or hide them from view by clicking 'Ignore'. Don't worry, you can always change your mind later. - .one-third.column.omega - %button.fullwidth{ type: 'button', ng: { click: 'addingNewVariants = false' } } - %i.icon-chevron-left - Back to my inventory - -# .one-third.column - -# %button.text-big.fullwidth{ type: 'button', ng: { click: 'setVisibility(hub.id,variantIDs(filteredNewProducts),true)' } } - -# %i.icon-plus - -# Add All ({{filteredNewProducts.length}}) - -# .one-third.column.omega - -# %button.text-big.fullwidth{ type: 'button', ng: { click: 'setVisibility(hub.id,variantIDs(filteredNewProducts),false)' } } - -# %i.icon-remove - -# Ignore All ({{filteredNewProducts.length}}) - - %h2#no_results{ ng: { show: 'filteredNewProducts.length == 0' } } - No new products match the filters provided. - - %table#new-variants{ ng: { show: 'filteredNewProducts.length > 0' } } - %col.producer{ width: "20%" } - %col.product{ width: "20%" } - %col.variant{ width: "40%" } - %col.add{ width: "10%" } - %col.ignore{ width: "10%" } - %thead - %tr - %th.producer Producer - %th.product Product - %th.variant Variant - %th.add Add - %th.ignore Ignore - %tbody{ bindonce: true, ng: { repeat: 'product in filteredNewProducts = (newProducts = (products | hubPermissions:hubPermissions:hub.id | newInventoryProducts:hub.id) | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } - %tr{ id: "nv_{{variant.id}}", ng: { repeat: 'variant in product.variants | newInventoryVariants:hub.id'} } - %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } - %td.product{ bo: { bind: 'product.name'} } - %td.variant - %span{ bo: { bind: 'variant.display_name || ""'} } - .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } - %td.add - %input.fullwidth{ :type => 'button', value: "Add", ng: { click: "setVisibility(hub.id,variant.id,true)" } } - %td.ignore - %input.fullwidth{ :type => 'button', value: "Ignore", ng: { click: "setVisibility(hub.id,variant.id,false)" } } - - .sixteen.columns.alpha.omega.text-center{ ng: {show: 'productLimit < filteredNewProducts.length'}} - %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } - or - %input{ type: 'button', value: "Show All ({{ filteredNewProducts.length - productLimit }} More)", ng: { click: 'productLimit = filteredNewProducts.length' } } diff --git a/app/views/admin/variant_overrides/_no_results.html.haml b/app/views/admin/variant_overrides/_no_results.html.haml new file mode 100644 index 0000000000..be274faf04 --- /dev/null +++ b/app/views/admin/variant_overrides/_no_results.html.haml @@ -0,0 +1,7 @@ +%div.text-big.no-results{ ng: { show: 'hub && products.length > 0 && filteredProducts.length == 0' } } + %span{ ng: { show: 'views.inventory.visible && !filtersApplied()' } } You inventory is currently empty. + %span{ ng: { show: 'views.inventory.visible && filtersApplied()' } } No matching products found in your inventory. + %span{ ng: { show: 'views.hidden.visible && !filtersApplied()' } } No products have been hidden from this inventory. + %span{ ng: { show: 'views.hidden.visible && filtersApplied()' } } No hidden products match your search criteria. + %span{ ng: { show: 'views.new.visible && !filtersApplied()' } } No new products available to add this inventory. + %span{ ng: { show: 'views.new.visible && filtersApplied()' } } No new products match your search criteria. diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index 81fde749b3..9032f519ca 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -1,30 +1,27 @@ -%table.index.bulk#variant-overrides - %col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } } - %col.product{ width: "20%", ng: { show: 'columns.product.visible' } } - %col.sku{ width: "20%", ng: { show: 'columns.sku.visible' } } - %col.price{ width: "10%", ng: { show: 'columns.price.visible' } } - %col.on_hand{ width: "10%", ng: { show: 'columns.on_hand.visible' } } - %col.on_demand{ width: "10%", ng: { show: 'columns.on_demand.visible' } } - %col.reset{ width: "1%", ng: { show: 'columns.reset.visible' } } - %col.reset{ width: "15%", ng: { show: 'columns.reset.visible' } } - %col.inheritance{ width: "5%", ng: { show: 'columns.inheritance.visible' } } - %col.visibility{ width: "10%", ng: { show: 'columns.visibility.visible' } } - %thead - %tr{ ng: { controller: "ColumnsCtrl" } } - %th.producer{ ng: { show: 'columns.producer.visible' } } Producer - %th.product{ ng: { show: 'columns.product.visible' } } Product - %th.sku{ ng: { show: 'columns.sku.visible' } } SKU - %th.price{ ng: { show: 'columns.price.visible' } } Price - %th.on_hand{ ng: { show: 'columns.on_hand.visible' } } On hand - %th.on_demand{ ng: { show: 'columns.on_demand.visible' } } On Demand? - %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } } Enable Stock Level Reset? - %th.inheritance{ ng: { show: 'columns.inheritance.visible' } } Inherit? - %th.visibility{ ng: { show: 'columns.visibility.visible' } } Show/Hide? - %tbody{bindonce: true, ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub.id | inventoryProducts:hub.id:showHidden | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } - = render 'admin/variant_overrides/products_product' - = render 'admin/variant_overrides/products_variants' - -.sixteen.columns.alpha.omega.text-center{ ng: {show: 'productLimit < filteredProducts.length'}} - %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } - or - %input{ type: 'button', value: "Show All ({{ filteredProducts.length - productLimit }} More)", ng: { click: 'productLimit = filteredProducts.length' } } +%form{ name: 'variant_overrides_form', ng: { show: "views.inventory.visible" } } + %save-bar{ save: "update()", form: "variant_overrides_form" } + %table.index.bulk#variant-overrides + %col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } } + %col.product{ width: "20%", ng: { show: 'columns.product.visible' } } + %col.sku{ width: "20%", ng: { show: 'columns.sku.visible' } } + %col.price{ width: "10%", ng: { show: 'columns.price.visible' } } + %col.on_hand{ width: "10%", ng: { show: 'columns.on_hand.visible' } } + %col.on_demand{ width: "10%", ng: { show: 'columns.on_demand.visible' } } + %col.reset{ width: "1%", ng: { show: 'columns.reset.visible' } } + %col.reset{ width: "15%", ng: { show: 'columns.reset.visible' } } + %col.inheritance{ width: "5%", ng: { show: 'columns.inheritance.visible' } } + %col.visibility{ width: "10%", ng: { show: 'columns.visibility.visible' } } + %thead + %tr{ ng: { controller: "ColumnsCtrl" } } + %th.producer{ ng: { show: 'columns.producer.visible' } } Producer + %th.product{ ng: { show: 'columns.product.visible' } } Product + %th.sku{ ng: { show: 'columns.sku.visible' } } SKU + %th.price{ ng: { show: 'columns.price.visible' } } Price + %th.on_hand{ ng: { show: 'columns.on_hand.visible' } } On hand + %th.on_demand{ ng: { show: 'columns.on_demand.visible' } } On Demand? + %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } } Enable Stock Level Reset? + %th.inheritance{ ng: { show: 'columns.inheritance.visible' } } Inherit? + %th.visibility{ ng: { show: 'columns.visibility.visible' } } Hide + %tbody{bindonce: true, ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub.id | inventoryProducts:hub.id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } + = render 'admin/variant_overrides/products_product' + = render 'admin/variant_overrides/products_variants' diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index 1af6dab3d8..7ae5f09db3 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -1,4 +1,4 @@ -%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants | inventoryVariants:hub.id:showHidden'}} +%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants | inventoryVariants:hub.id:views'}} %td.producer{ ng: { show: 'columns.producer.visible' } } %td.product{ ng: { show: 'columns.product.visible' } } %span{ bo: { bind: 'variant.display_name || ""'} } @@ -18,4 +18,5 @@ %td.inheritance{ ng: { show: 'columns.inheritance.visible' } } %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-inherit', ng: { model: 'inherit' }, 'track-inheritance' => true } %td.visibility{ ng: { show: 'columns.visibility.visible' } } - %input.fullwidth{ :type => 'button', ng: { value: "inventoryItems[hub.id][variant.id].visible ? 'Hide' : 'Show'", click: "setVisibility(hub.id,variant.id,!inventoryItems[hub.id][variant.id].visible)", class: "{ hidden: !inventoryItems[hub.id][variant.id].visible}" } } + %button.icon-remove.hide.fullwidth{ :type => 'button', ng: { click: "setVisibility(hub.id,variant.id,false)" } } + = t(:hide) diff --git a/app/views/admin/variant_overrides/_show_more.html.haml b/app/views/admin/variant_overrides/_show_more.html.haml new file mode 100644 index 0000000000..ad943c853e --- /dev/null +++ b/app/views/admin/variant_overrides/_show_more.html.haml @@ -0,0 +1,4 @@ +.sixteen.columns.alpha.omega.text-center{ ng: {show: 'productLimit < filteredProducts.length'}} + %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } + or + %input{ type: 'button', value: "Show All ({{ filteredProducts.length - productLimit }} More)", ng: { click: 'productLimit = filteredProducts.length' } } diff --git a/app/views/admin/variant_overrides/index.html.haml b/app/views/admin/variant_overrides/index.html.haml index 5c7a250cb9..d939465bac 100644 --- a/app/views/admin/variant_overrides/index.html.haml +++ b/app/views/admin/variant_overrides/index.html.haml @@ -3,24 +3,12 @@ .margin-bottom-50{ ng: { app: 'admin.variantOverrides', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } } = render 'admin/variant_overrides/filters' - %div.sixteen.columns.alpha#loading{ ng: { cloak: true, if: 'hub && products.length == 0 && RequestMonitor.loading' } } - %img.spinner{ src: "/assets/spinning-circles.svg" } - %h1 LOADING INVENTORY - = render 'admin/variant_overrides/new_variants' - %span.text-big.no-results{ ng: { show: 'hub && !addingNewVariants && products.length > 0 && filteredProducts.length == 0' } } - No products matching products found. - %span.text-big.no-results{ ng: { show: 'hub && !addingNewVariants && products.length == 0 && !RequestMonitor.loading' } } - There are no products in {{ hub.name }}'s inventory - %button{ value: 'Add Some!' } - %div{ ng: { cloak: true, show: 'hub && !addingNewVariants && filteredProducts.length > 0' } } - %hr.divider.sixteen.columns.alpha.omega - .controls.sixteen.columns.alpha.omega - %input.four.columns.alpha{ type: 'button', value: 'Reset Stock to Defaults', 'ng-click' => 'resetStock()' } - %input.four.columns{ type: 'button', ng: { value: "showHidden ? 'Hide Hidden' : 'Show Hidden'", click: 'showHidden = !showHidden' } } - %div.five.columns   - = render 'admin/shared/columns_dropdown' - - - %form{ name: 'variant_overrides_form' } - %save-bar{ save: "update()", form: "variant_overrides_form" } - = render 'admin/variant_overrides/products' + = render 'admin/variant_overrides/new_products_alert' + = render 'admin/variant_overrides/loading_flash' + = render 'admin/variant_overrides/controls' + = render 'admin/variant_overrides/no_results' + %div{ ng: { cloak: true, show: 'hub && filteredProducts.length > 0' } } + = render 'admin/variant_overrides/new_products' + = render 'admin/variant_overrides/hidden_products' + = render 'admin/variant_overrides/products' + = render 'admin/variant_overrides/show_more' diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index fafcb348db..392b5b48b3 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -99,23 +99,24 @@ feature %q{ # Show/Hide products first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Show/Hide").click + first("div#columns-dropdown div.menu div.menu_item", text: "Hide").click first("div#columns-dropdown", :text => "COLUMNS").click expect(page).to have_selector "tr#v_#{variant.id}" expect(page).to have_selector "tr#v_#{variant_related.id}" within "tr#v_#{variant.id}" do click_button 'Hide' end expect(page).to_not have_selector "tr#v_#{variant.id}" expect(page).to have_selector "tr#v_#{variant_related.id}" - click_button 'Show Hidden' - expect(page).to have_selector "tr#v_#{variant.id}" - expect(page).to have_selector "tr#v_#{variant_related.id}" - within "tr#v_#{variant.id}" do click_button 'Show' end - within "tr#v_#{variant_related.id}" do click_button 'Hide' end - expect(page).to have_selector "tr#v_#{variant.id}" - expect(page).to have_selector "tr#v_#{variant_related.id}" - click_button 'Hide Hidden' + first("div#views-dropdown").click + first("div#views-dropdown div.menu div.menu_item", text: "Hidden Products").click expect(page).to have_selector "tr#v_#{variant.id}" expect(page).to_not have_selector "tr#v_#{variant_related.id}" + within "tr#v_#{variant.id}" do click_button 'Add' end + expect(page).to_not have_selector "tr#v_#{variant.id}" + expect(page).to_not have_selector "tr#v_#{variant_related.id}" + first("div#views-dropdown").click + first("div#views-dropdown div.menu div.menu_item", text: "Inventory Products").click + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" end it "creates new overrides" do @@ -271,7 +272,8 @@ feature %q{ end it "resets stock to defaults" do - click_button 'Reset Stock to Defaults' + first("div#bulk-actions-dropdown").click + first("div#bulk-actions-dropdown div.menu div.menu_item", text: "Reset Stock Levels To Defaults").click page.should have_content 'Stocks reset to defaults.' vo.reload page.should have_input "variant-overrides-#{variant.id}-count_on_hand", with: '1000', placeholder: '12' @@ -279,7 +281,8 @@ feature %q{ end it "doesn't reset stock levels if the behaviour is disabled" do - click_button 'Reset Stock to Defaults' + first("div#bulk-actions-dropdown").click + first("div#bulk-actions-dropdown div.menu div.menu_item", text: "Reset Stock Levels To Defaults").click vo_no_reset.reload page.should have_input "variant-overrides-#{variant2.id}-count_on_hand", with: '40', placeholder: '12' vo_no_reset.count_on_hand.should == 40 @@ -287,7 +290,8 @@ feature %q{ it "prompts to save changes before reset if any are pending" do fill_in "variant-overrides-#{variant.id}-price", with: '200' - click_button 'Reset Stock to Defaults' + first("div#bulk-actions-dropdown").click + first("div#bulk-actions-dropdown div.menu div.menu_item", text: "Reset Stock Levels To Defaults").click page.should have_content "Save changes first" end end @@ -305,20 +309,30 @@ feature %q{ select2_select hub.name, from: 'hub_id' end - it "shows new variants, and allows them to be added or ignored" do + it "alerts the user to the presence of new products, and allows them to be added or hidden" do expect(page).to_not have_selector "table#variant-overrides tr#v_#{variant1.id}" expect(page).to_not have_selector "table#variant-overrides tr#v_#{variant2.id}" - expect(page).to have_table_row ['PRODUCER', 'PRODUCT', 'VARIANT', 'ADD', 'IGNORE'] - expect(page).to have_selector "table#new-variants tr#nv_#{variant1.id}" - expect(page).to have_selector "table#new-variants tr#nv_#{variant2.id}" - within "table#new-variants tr#nv_#{variant1.id}" do click_button 'Add' end - within "table#new-variants tr#nv_#{variant2.id}" do click_button 'Ignore' end - expect(page).to_not have_selector "table#new-variants tr#nv_#{variant1.id}" - expect(page).to_not have_selector "table#new-variants tr#nv_#{variant2.id}" + expect(page).to have_selector '.alert-row span.message', text: "There are 1 new products available to add to your inventory." + click_button "Review Now" + + expect(page).to have_table_row ['PRODUCER', 'PRODUCT', 'VARIANT', 'ADD', 'HIDE'] + expect(page).to have_selector "table#new-products tr#v_#{variant1.id}" + expect(page).to have_selector "table#new-products tr#v_#{variant2.id}" + within "table#new-products tr#v_#{variant1.id}" do click_button 'Add' end + within "table#new-products tr#v_#{variant2.id}" do click_button 'Hide' end + expect(page).to_not have_selector "table#new-products tr#v_#{variant1.id}" + expect(page).to_not have_selector "table#new-products tr#v_#{variant2.id}" + click_button "Back to my inventory" expect(page).to have_selector "table#variant-overrides tr#v_#{variant1.id}" expect(page).to_not have_selector "table#variant-overrides tr#v_#{variant2.id}" + + first("div#views-dropdown").click + first("div#views-dropdown div.menu div.menu_item", text: "Hidden Products").click + + expect(page).to_not have_selector "table#hidden-products tr#v_#{variant1.id}" + expect(page).to have_selector "table#hidden-products tr#v_#{variant2.id}" end end end diff --git a/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee new file mode 100644 index 0000000000..7333882d5d --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee @@ -0,0 +1,37 @@ +describe "Views service", -> + Views = null + + beforeEach -> + module 'admin.indexUtils' + + inject (_Views_) -> + Views = _Views_ + + describe "setting views", -> + beforeEach -> + spyOn(Views, "selectView").andCallThrough() + Views.setViews + view1: { name: 'View1', visible: true } + view2: { name: 'View2', visible: false } + view3: { name: 'View3', visible: true } + + it "sets resets @views and copies each view of the provided object across", -> + expect(Object.keys(Views.views)).toEqual ['view1', 'view2', 'view3'] + + it "calls selectView if visible is true", -> + expect(Views.selectView).toHaveBeenCalledWith('view1') + expect(Views.selectView).not.toHaveBeenCalledWith('view2'); + expect(Views.selectView).toHaveBeenCalledWith('view3') + expect(view.visible for key, view of Views.views).toEqual [false, false, true] + + describe "selecting a view", -> + beforeEach -> + Views.currentView = "some View" + Views.views = { view7: { name: 'View7', visible: false } } + Views.selectView('view7') + + it "sets the currentView", -> + expect(Views.currentView.name).toEqual 'View7' + + it "switches the visibility of the given view", -> + expect(Views.currentView).toEqual { name: 'View7', visible: true } From f05f88c1cb49b33a803883a078999279eabc991f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 10 Feb 2016 16:42:52 +1100 Subject: [PATCH 35/54] Adding global config to allow shop users to only select from inventory variants in OC interface --- .../visible_product_variants.js.coffee | 4 -- .../filters/visible_products.js.coffee | 2 +- .../filters/visible_variants.js.coffee | 3 + app/models/enterprise.rb | 5 ++ app/models/spree/variant_decorator.rb | 9 +++ .../api/admin/enterprise_serializer.rb | 1 + .../api/admin/exchange_serializer.rb | 14 +++- .../api/admin/order_cycle_serializer.rb | 9 ++- .../form/_shop_preferences.html.haml | 14 ++++ ...change_distributed_products_form.html.haml | 3 +- spec/models/spree/variant_spec.rb | 29 ++++++++ .../admin/exchange_serializer_spec.rb | 66 +++++++++++++++---- 12 files changed, 137 insertions(+), 22 deletions(-) delete mode 100644 app/assets/javascripts/admin/order_cycles/filters/visible_product_variants.js.coffee create mode 100644 app/assets/javascripts/admin/order_cycles/filters/visible_variants.js.coffee diff --git a/app/assets/javascripts/admin/order_cycles/filters/visible_product_variants.js.coffee b/app/assets/javascripts/admin/order_cycles/filters/visible_product_variants.js.coffee deleted file mode 100644 index d2e69ad64f..0000000000 --- a/app/assets/javascripts/admin/order_cycles/filters/visible_product_variants.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module("admin.orderCycles").filter "visibleProductVariants", -> - return (product, exchange, rules) -> - variants = product.variants.concat( [{ "id": product.master_id}] ) - return (variant for variant in variants when variant.id in rules[exchange.enterprise_id]) diff --git a/app/assets/javascripts/admin/order_cycles/filters/visible_products.js.coffee b/app/assets/javascripts/admin/order_cycles/filters/visible_products.js.coffee index 40586854c1..5ce498b998 100644 --- a/app/assets/javascripts/admin/order_cycles/filters/visible_products.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/filters/visible_products.js.coffee @@ -1,3 +1,3 @@ angular.module("admin.orderCycles").filter "visibleProducts", ($filter) -> return (products, exchange, rules) -> - return (product for product in products when $filter('visibleProductVariants')(product, exchange, rules).length > 0) + return (product for product in products when $filter('visibleVariants')(product.variants, exchange, rules).length > 0) diff --git a/app/assets/javascripts/admin/order_cycles/filters/visible_variants.js.coffee b/app/assets/javascripts/admin/order_cycles/filters/visible_variants.js.coffee new file mode 100644 index 0000000000..db261ebcea --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/filters/visible_variants.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.orderCycles").filter "visibleVariants", -> + return (variants, exchange, rules) -> + return (variant for variant in variants when variant.id in rules[exchange.enterprise_id]) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 66f990177d..162c4c8e29 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -8,6 +8,11 @@ class Enterprise < ActiveRecord::Base preference :shopfront_taxon_order, :string, default: "" preference :shopfront_order_cycle_order, :string, default: "orders_close_at" + # This is hopefully a temporary measure, pending the arrival of multiple named inventories + # for shops. We need this here to allow hubs to restrict visible variants to only those in + # their inventory if they so choose + preference :product_selection_from_inventory_only, :boolean, default: false + devise :confirmable, reconfirmable: true, confirmation_keys: [ :id, :email ] handle_asynchronously :send_confirmation_instructions handle_asynchronously :send_on_create_confirmation_instructions diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 33e7f5bffd..1dd93c9626 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -12,6 +12,7 @@ Spree::Variant.class_eval do has_many :exchange_variants, dependent: :destroy has_many :exchanges, through: :exchange_variants has_many :variant_overrides + has_many :inventory_items attr_accessible :unit_value, :unit_description, :images_attributes, :display_as, :display_name accepts_nested_attributes_for :images @@ -40,6 +41,14 @@ Spree::Variant.class_eval do where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor)) } + scope :visible_for, lambda { |enterprise| + joins(:inventory_items).where('inventory_items.enterprise_id = (?) AND inventory_items.visible = (?)', enterprise, true) + } + scope :not_hidden_for, lambda { |enterprise| + joins('LEFT OUTER JOIN inventory_items ON inventory_items.variant_id = spree_variants.id') + .where('inventory_items.id IS NULL OR (inventory_items.enterprise_id = (?) AND inventory_items.visible != (?))', enterprise, false) + } + # Define sope as class method to allow chaining with other scopes filtering id. # In Rails 3, merging two scopes on the same column will consider only the last scope. def self.in_distributor(distributor) diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 8f955bdae2..37a96b402f 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -2,6 +2,7 @@ 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, :preferred_shopfront_order_cycle_order + attributes :preferred_product_selection_from_inventory_only attributes :owner, :users has_one :owner, serializer: Api::Admin::UserSerializer diff --git a/app/serializers/api/admin/exchange_serializer.rb b/app/serializers/api/admin/exchange_serializer.rb index cb49d94fc8..4ef9b8964f 100644 --- a/app/serializers/api/admin/exchange_serializer.rb +++ b/app/serializers/api/admin/exchange_serializer.rb @@ -9,8 +9,18 @@ class Api::Admin::ExchangeSerializer < ActiveModel::Serializer permitted = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle). visible_variants_for_incoming_exchanges_from(object.sender) else - permitted = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle). - visible_variants_for_outgoing_exchanges_to(object.receiver) + # This is hopefully a temporary measure, pending the arrival of multiple named inventories + # for shops. We need this here to allow hubs to restrict visible variants to only those in + # their inventory if they so choose + permitted = if object.receiver.prefers_product_selection_from_inventory_only? + OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle) + .visible_variants_for_outgoing_exchanges_to(object.receiver) + .visible_for(object.receiver) + else + OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle) + .visible_variants_for_outgoing_exchanges_to(object.receiver) + .not_hidden_for(object.receiver) + end end Hash[ object.variants.merge(permitted).map { |v| [v.id, true] } ] end diff --git a/app/serializers/api/admin/order_cycle_serializer.rb b/app/serializers/api/admin/order_cycle_serializer.rb index 53b1a10c04..8068055df2 100644 --- a/app/serializers/api/admin/order_cycle_serializer.rb +++ b/app/serializers/api/admin/order_cycle_serializer.rb @@ -58,7 +58,14 @@ class Api::Admin::OrderCycleSerializer < ActiveModel::Serializer permissions = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object) enterprises = permissions.visible_enterprises enterprises.each do |enterprise| - variants = permissions.visible_variants_for_outgoing_exchanges_to(enterprise).pluck(:id) + # This is hopefully a temporary measure, pending the arrival of multiple named inventories + # for shops. We need this here to allow hubs to restrict visible variants to only those in + # their inventory if they so choose + variants = if enterprise.prefers_product_selection_from_inventory_only? + permissions.visible_variants_for_outgoing_exchanges_to(enterprise).visible_for(enterprise) + else + permissions.visible_variants_for_outgoing_exchanges_to(enterprise).not_hidden_for(enterprise) + end.pluck(:id) visible[enterprise.id] = variants if variants.any? end visible diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index 3085727f85..f732d98a4c 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -33,3 +33,17 @@ .five.columns.omega = radio_button :enterprise, :preferred_shopfront_order_cycle_order, :orders_close_at, { 'ng-model' => 'Enterprise.preferred_shopfront_order_cycle_order' } = label :enterprise, :preferred_shopfront_order_cycle_order_orders_close_at, "Close Date" + +-# This is hopefully a temporary measure, pending the arrival of multiple named inventories +-# for shops. We need this here to allow hubs to restrict visible variants to only those in +-# their inventory if they so choose +.row + .alpha.eleven.columns + .three.columns.alpha + = f.label "enterprise_preferred_product_selection_from_inventory_only", t(:select_products_for_order_cycle_from) + .three.columns + = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "1", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } + = label :enterprise, :preferred_product_selection_from_inventory_only, "Inventory Only" + .five.columns.omega + = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "0", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } + = label :enterprise, :preferred_product_selection_from_inventory_only, "All Available Products (Ignore Inventory)" 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 78e64fc2d0..cff8f5d2b3 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 @@ -15,8 +15,7 @@ .name {{ product.name }} .supplier {{ product.supplier_name }} - -# if we ever need to filter variants within a product using visibility permissions, we can use this filter: visibleVariants:exchange:order_cycle.visible_variants_for_outgoing_exchanges - .exchange-product-variant{'ng-repeat' => 'variant in product.variants | filter:variantSuppliedToOrderCycle'} + .exchange-product-variant{'ng-repeat' => 'variant in product.variants | visibleVariants:exchange:order_cycle.visible_variants_for_outgoing_exchanges | filter:variantSuppliedToOrderCycle'} %label = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', 1, 1, 'ng-model' => 'exchange.variants[variant.id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', 'ng-disabled' => '!order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(variant.id) < 0' diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index cd6bfc1bd3..1f5aeeba18 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -110,6 +110,35 @@ module Spree p_external.variants.for_distribution(oc, d1).should be_empty end end + + describe "finding variants based on visiblity in inventory" do + let(:enterprise) { create(:distributor_enterprise) } + let!(:new_variant) { create(:variant) } + let!(:hidden_variant) { create(:variant) } + let!(:visible_variant) { create(:variant) } + + let!(:hidden_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: hidden_variant, visible: false ) } + let!(:visible_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: visible_variant, visible: true ) } + + + context "finding variants that are not hidden from an enterprise's inventory" do + let!(:variants) { Spree::Variant.not_hidden_for(enterprise) } + + it "lists any variants that are not listed as visible=false" do + expect(variants).to include new_variant, visible_variant + expect(variants).to_not include hidden_variant + end + end + + context "finding variants that are visible in an enterprise's inventory" do + let!(:variants) { Spree::Variant.visible_for(enterprise) } + + it "lists any variants that are not listed as visible=false" do + expect(variants).to include visible_variant + expect(variants).to_not include new_variant, hidden_variant + end + end + end end describe "indexing variants by id" do diff --git a/spec/serializers/admin/exchange_serializer_spec.rb b/spec/serializers/admin/exchange_serializer_spec.rb index b45f6bcb9b..ecb85d07d8 100644 --- a/spec/serializers/admin/exchange_serializer_spec.rb +++ b/spec/serializers/admin/exchange_serializer_spec.rb @@ -3,24 +3,66 @@ require 'open_food_network/order_cycle_permissions' describe Api::Admin::ExchangeSerializer do let(:v1) { create(:variant) } let(:v2) { create(:variant) } - let(:exchange) { create(:exchange, incoming: false, variants: [v1, v2]) } let(:permissions_mock) { double(:permissions) } let(:serializer) { Api::Admin::ExchangeSerializer.new exchange } + context "serializing incoming exchanges" do + let(:exchange) { create(:exchange, incoming: true, variants: [v1, v2]) } + let(:permitted_variants) { double(:permitted_variants) } - before do - allow(OpenFoodNetwork::OrderCyclePermissions).to receive(:new) { permissions_mock } - allow(permissions_mock).to receive(:visible_variants_for_outgoing_exchanges_to) do - # This is the permitted list of variants - Spree::Variant.where(id: [v1] ) + before do + allow(OpenFoodNetwork::OrderCyclePermissions).to receive(:new) { permissions_mock } + allow(permissions_mock).to receive(:visible_variants_for_incoming_exchanges_from) { Spree::Variant.where(id: [v1]) } + end + + it "filters variants within the exchange based on permissions" do + visible_variants = serializer.variants + expect(permissions_mock).to have_received(:visible_variants_for_incoming_exchanges_from).with(exchange.sender) + expect(exchange.variants).to include v1, v2 + expect(visible_variants.keys).to include v1.id + expect(visible_variants.keys).to_not include v2.id end end - it "filters variants within the exchange based on permissions" do - visible_variants = serializer.variants - expect(permissions_mock).to have_received(:visible_variants_for_outgoing_exchanges_to).with(exchange.receiver) - expect(exchange.variants).to include v1, v2 - expect(visible_variants.keys).to include v1.id - expect(visible_variants.keys).to_not include v2.id + context "serializing outgoing exchanges" do + let(:exchange) { create(:exchange, incoming: false, variants: [v1, v2]) } + let(:permitted_variants) { double(:permitted_variants) } + + before do + allow(OpenFoodNetwork::OrderCyclePermissions).to receive(:new) { permissions_mock } + allow(permissions_mock).to receive(:visible_variants_for_outgoing_exchanges_to) { permitted_variants } + end + + context "when the receiver prefers to see all variants (not just those in their inventory)" do + before do + allow(exchange.receiver).to receive(:prefers_product_selection_from_inventory_only?) { false } + allow(permitted_variants).to receive(:not_hidden_for) { Spree::Variant.where(id: [v1]) } + end + + it "filters variants within the exchange based on permissions" do + visible_variants = serializer.variants + expect(permissions_mock).to have_received(:visible_variants_for_outgoing_exchanges_to).with(exchange.receiver) + expect(permitted_variants).to have_received(:not_hidden_for).with(exchange.receiver) + expect(exchange.variants).to include v1, v2 + expect(visible_variants.keys).to include v1.id + expect(visible_variants.keys).to_not include v2.id + end + end + + context "when the receiver prefers to restrict visible variants to only those in their inventory" do + before do + allow(exchange.receiver).to receive(:prefers_product_selection_from_inventory_only?) { true } + allow(permitted_variants).to receive(:visible_for) { Spree::Variant.where(id: [v1]) } + end + + it "filters variants within the exchange based on permissions, and inventory visibility" do + visible_variants = serializer.variants + expect(permissions_mock).to have_received(:visible_variants_for_outgoing_exchanges_to).with(exchange.receiver) + expect(permitted_variants).to have_received(:visible_for).with(exchange.receiver) + expect(exchange.variants).to include v1, v2 + expect(visible_variants.keys).to include v1.id + expect(visible_variants.keys).to_not include v2.id + end + end end end From 488daed8f34206718d804bcc0683fab6fd246bb7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 10 Feb 2016 16:43:20 +1100 Subject: [PATCH 36/54] Translating variant overrides / inventory page --- .../shared/_bulk_actions_dropdown.html.haml | 2 +- .../admin/shared/_columns_dropdown.html.haml | 2 +- .../admin/shared/_views_dropdown.html.haml | 2 +- .../variant_overrides/_controls.html.haml | 6 ++-- .../variant_overrides/_filters.html.haml | 8 ++--- .../_hidden_products.html.haml | 10 +++--- .../variant_overrides/_new_products.html.haml | 14 ++++---- .../_new_products_alert.html.haml | 4 +-- .../variant_overrides/_no_results.html.haml | 12 +++---- .../variant_overrides/_products.html.haml | 18 +++++----- .../_products_variants.html.haml | 2 +- config/locales/en.yml | 36 +++++++++++++++++++ 12 files changed, 76 insertions(+), 40 deletions(-) diff --git a/app/views/admin/shared/_bulk_actions_dropdown.html.haml b/app/views/admin/shared/_bulk_actions_dropdown.html.haml index 15959a6d15..0c8ac170a1 100644 --- a/app/views/admin/shared/_bulk_actions_dropdown.html.haml +++ b/app/views/admin/shared/_bulk_actions_dropdown.html.haml @@ -1,5 +1,5 @@ .ofn-drop-down#bulk-actions-dropdown - %span.icon-check   Actions + %span.icon-check= "  #{t('admin.actions')}".html_safe %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } .menu{ 'ng-show' => "expanded" } .menu_item{ 'ng-repeat' => "action in bulkActions", 'ng-click' => "$eval(action.callback)(filteredLineItems)", 'close-on-click' => true } diff --git a/app/views/admin/shared/_columns_dropdown.html.haml b/app/views/admin/shared/_columns_dropdown.html.haml index ee8e552bb3..4663443321 100644 --- a/app/views/admin/shared/_columns_dropdown.html.haml +++ b/app/views/admin/shared/_columns_dropdown.html.haml @@ -1,5 +1,5 @@ .ofn-drop-down.right#columns-dropdown - %span{ :class => 'icon-reorder' }   Columns + %span{ :class => 'icon-reorder' }= "  #{t('admin.columns')}".html_safe %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } %div.menu{ 'ng-show' => "expanded" } %div.menu_item{ ng: { repeat: "column in columns" }, toggle: { column: true } } diff --git a/app/views/admin/shared/_views_dropdown.html.haml b/app/views/admin/shared/_views_dropdown.html.haml index addf43a1ce..9fe3df8521 100644 --- a/app/views/admin/shared/_views_dropdown.html.haml +++ b/app/views/admin/shared/_views_dropdown.html.haml @@ -1,5 +1,5 @@ .ofn-drop-down#views-dropdown - %span{ :class => 'icon-eye-open' }   Viewing: {{ currentView().name }} + %span{ :class => 'icon-eye-open' }= "  #{t('admin.viewing', current_view_name: '{{ currentView().name }}')}".html_safe %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } %div.menu{ 'ng-show' => "expanded" } %div.menu_item{ ng: { repeat: "(viewKey, view) in views" }, toggle: { view: true }, 'close-on-click' => true } diff --git a/app/views/admin/variant_overrides/_controls.html.haml b/app/views/admin/variant_overrides/_controls.html.haml index bb70f2cf5a..3027ad7082 100644 --- a/app/views/admin/variant_overrides/_controls.html.haml +++ b/app/views/admin/variant_overrides/_controls.html.haml @@ -3,9 +3,9 @@ .eight.columns.alpha = render 'admin/shared/bulk_actions_dropdown' = render 'admin/shared/views_dropdown' - %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.inventory.visible' } , data: { powertip: 'This is your inventory of products. Products must be listed here before you can sell them in your shop. To add products to your inventory, select \'New Products\' from the Viewing dropdown.' } } - %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.hidden.visible' } , data: { powertip: 'These products have been hidden from your inventory and will not be available in your shop. You can click \'Add\' to add a product to you inventory.' } } - %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.new.visible' } , data: { powertip: 'These products are available to be added to your inventory. Click \'Add\' to add a product to your inventory, or \'Hide\' to hide it from view. You can always change your mind later!' } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.inventory.visible' } , data: { powertip: "#{t('admin.inventory.inventory_powertip')}" } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.hidden.visible' } , data: { powertip: "#{t('admin.inventory.hidden_powertip')}" } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.new.visible' } , data: { powertip: "#{t('admin.inventory.new_powertip')}" } } .four.columns   .four.columns.omega{ ng: { show: 'views.new.visible' } } %button.fullwidth{ type: 'button', ng: { click: "selectView('inventory')" } } diff --git a/app/views/admin/variant_overrides/_filters.html.haml b/app/views/admin/variant_overrides/_filters.html.haml index d5f8bb9714..90c5e15c4f 100644 --- a/app/views/admin/variant_overrides/_filters.html.haml +++ b/app/views/admin/variant_overrides/_filters.html.haml @@ -1,15 +1,15 @@ .filters.sixteen.columns.alpha .filter.four.columns.alpha - %label{ :for => 'query', ng: {class: '{disabled: !hub.id}'} }Quick Search + %label{ :for => 'query', ng: {class: '{disabled: !hub.id}'} }=t('admin.quick_search') %br %input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query', disabled: '!hub.id'} } .two.columns   .filter_select.four.columns - %label{ :for => 'hub_id', ng: { bind: 'hub_id ? "Shop" : "Select a shop"' } } + %label{ :for => 'hub_id', ng: { bind: "hub_id ? '#{t('admin.shop')}' : '#{t('admin.inventory.select_a_shop')}'" } } %br %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', ng: { options: 'hub.id as hub.name for (id, hub) in hubs', change: 'selectHub()' } } .filter_select.four.columns - %label{ :for => 'producer_filter', ng: {class: '{disabled: !hub.id}'} }Producer + %label{ :for => 'producer_filter', ng: {class: '{disabled: !hub.id}'} }=t('admin.producer') %br %input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', data: 'producers', blank: "{id: 0, name: 'All'}", ng: { model: 'producerFilter', disabled: '!hub.id' } } -# .filter_select{ :class => "three columns" } @@ -23,4 +23,4 @@ .filter_clear.two.columns.omega %label{ :for => 'clear_all_filters' } %br - %input.red.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "Clear All", ng: { click: "resetSelectFilters()", disabled: '!hub.id'} } + %input.red.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "#{t('admin.clear_all')}", ng: { click: "resetSelectFilters()", disabled: '!hub.id'} } diff --git a/app/views/admin/variant_overrides/_hidden_products.html.haml b/app/views/admin/variant_overrides/_hidden_products.html.haml index 1ef143afe0..064b99d19a 100644 --- a/app/views/admin/variant_overrides/_hidden_products.html.haml +++ b/app/views/admin/variant_overrides/_hidden_products.html.haml @@ -6,10 +6,10 @@ %col.add{ width: "15%" } %thead %tr - %th.producer Producer - %th.product Product - %th.variant Variant - %th.add Add + %th.producer=t('admin.producer') + %th.product=t('admin.product') + %th.variant=t('(admin.variant') + %th.add=t('admin.inventory.add') %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub.id:views' } } %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } @@ -19,4 +19,4 @@ .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } %td.add %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub.id,variant.id,true)" } } - = t(:add) + = t('admin.inventory.add') diff --git a/app/views/admin/variant_overrides/_new_products.html.haml b/app/views/admin/variant_overrides/_new_products.html.haml index f4e519ce3f..affb52b066 100644 --- a/app/views/admin/variant_overrides/_new_products.html.haml +++ b/app/views/admin/variant_overrides/_new_products.html.haml @@ -6,11 +6,11 @@ %col.hide{ width: "15%" } %thead %tr - %th.producer Producer - %th.product Product - %th.variant Variant - %th.add Add - %th.hide Hide + %th.producer=t('admin.producer') + %th.product=t('admin.product') + %th.variant=t('(admin.variant') + %th.add=t('admin.inventory.add') + %th.hide=t('admin.inventory.hide') %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub.id:views' } } %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } @@ -20,7 +20,7 @@ .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } %td.add %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub.id,variant.id,true)" } } - = t(:add) + = t('admin.inventory.add') %td.hide %button.fullwidth.hide.icon-remove{ ng: { click: "setVisibility(hub.id,variant.id,false)" } } - = t(:hide) + = t('admin.inventory.hide') diff --git a/app/views/admin/variant_overrides/_new_products_alert.html.haml b/app/views/admin/variant_overrides/_new_products_alert.html.haml index 48b9d041c5..2d060033c5 100644 --- a/app/views/admin/variant_overrides/_new_products_alert.html.haml +++ b/app/views/admin/variant_overrides/_new_products_alert.html.haml @@ -1,5 +1,5 @@ %div{ ng: { show: '(newProductCount = (products | newInventoryProducts:hub_id).length) > 0 && !views.new.visible && !alertDismissed' } } %hr.divider.sixteen.columns.alpha.omega - %alert-row{ message: "There are {{ newProductCount }} new products available to add to your inventory.", + %alert-row{ message: "#{t('admin.inventory.new_products_alert_message', new_product_count: '{{ newProductCount }}')}", dismissed: "alertDismissed", - button: { text: 'Review Now', action: "selectView('new')" } } + button: { text: "#{t('admin.inventory.review_now')}", action: "selectView('new')" } } diff --git a/app/views/admin/variant_overrides/_no_results.html.haml b/app/views/admin/variant_overrides/_no_results.html.haml index be274faf04..628be090ad 100644 --- a/app/views/admin/variant_overrides/_no_results.html.haml +++ b/app/views/admin/variant_overrides/_no_results.html.haml @@ -1,7 +1,7 @@ %div.text-big.no-results{ ng: { show: 'hub && products.length > 0 && filteredProducts.length == 0' } } - %span{ ng: { show: 'views.inventory.visible && !filtersApplied()' } } You inventory is currently empty. - %span{ ng: { show: 'views.inventory.visible && filtersApplied()' } } No matching products found in your inventory. - %span{ ng: { show: 'views.hidden.visible && !filtersApplied()' } } No products have been hidden from this inventory. - %span{ ng: { show: 'views.hidden.visible && filtersApplied()' } } No hidden products match your search criteria. - %span{ ng: { show: 'views.new.visible && !filtersApplied()' } } No new products available to add this inventory. - %span{ ng: { show: 'views.new.visible && filtersApplied()' } } No new products match your search criteria. + %span{ ng: { show: 'views.inventory.visible && !filtersApplied()' } }=t('admin.inventory.currently_empty') + %span{ ng: { show: 'views.inventory.visible && filtersApplied()' } }=t('admin.inventory.no_matching_products') + %span{ ng: { show: 'views.hidden.visible && !filtersApplied()' } }=t('admin.inventory.no_hidden_products') + %span{ ng: { show: 'views.hidden.visible && filtersApplied()' } }=t('admin.inventory.no_matching_hidden_products') + %span{ ng: { show: 'views.new.visible && !filtersApplied()' } }=t('admin.inventory.no_new_products') + %span{ ng: { show: 'views.new.visible && filtersApplied()' } }=t('admin.inventory.no_matching_new_products') diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index 9032f519ca..d453dffbd7 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -13,15 +13,15 @@ %col.visibility{ width: "10%", ng: { show: 'columns.visibility.visible' } } %thead %tr{ ng: { controller: "ColumnsCtrl" } } - %th.producer{ ng: { show: 'columns.producer.visible' } } Producer - %th.product{ ng: { show: 'columns.product.visible' } } Product - %th.sku{ ng: { show: 'columns.sku.visible' } } SKU - %th.price{ ng: { show: 'columns.price.visible' } } Price - %th.on_hand{ ng: { show: 'columns.on_hand.visible' } } On hand - %th.on_demand{ ng: { show: 'columns.on_demand.visible' } } On Demand? - %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } } Enable Stock Level Reset? - %th.inheritance{ ng: { show: 'columns.inheritance.visible' } } Inherit? - %th.visibility{ ng: { show: 'columns.visibility.visible' } } Hide + %th.producer{ ng: { show: 'columns.producer.visible' } }=t('admin.producer') + %th.product{ ng: { show: 'columns.product.visible' } }=t('admin.product') + %th.sku{ ng: { show: 'columns.sku.visible' } }=t('admin.inventory.sku') + %th.price{ ng: { show: 'columns.price.visible' } }=t('admin.inventory.price') + %th.on_hand{ ng: { show: 'columns.on_hand.visible' } }=t('admin.inventory.on_hand') + %th.on_demand{ ng: { show: 'columns.on_demand.visible' } }=t('admin.inventory.on_demand') + %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } }=t('admin.inventory.enable_reset') + %th.inheritance{ ng: { show: 'columns.inheritance.visible' } }=t('admin.inventory.inherit') + %th.visibility{ ng: { show: 'columns.visibility.visible' } }=t('admin.inventory.hide') %tbody{bindonce: true, ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub.id | inventoryProducts:hub.id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } = render 'admin/variant_overrides/products_product' = render 'admin/variant_overrides/products_variants' diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index 7ae5f09db3..e9a9e12a15 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -19,4 +19,4 @@ %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-inherit', ng: { model: 'inherit' }, 'track-inheritance' => true } %td.visibility{ ng: { show: 'columns.visibility.visible' } } %button.icon-remove.hide.fullwidth{ :type => 'button', ng: { click: "setVisibility(hub.id,variant.id,false)" } } - = t(:hide) + = t('admin.inventory.hide') diff --git a/config/locales/en.yml b/config/locales/en.yml index a5c5ba7903..e5c0983ac2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -58,6 +58,42 @@ en: sort_order_cycles_on_shopfront_by: "Sort Order Cycles On Shopfront By" + + admin: + # General form elements + quick_search: Quick Search + clear_all: Clear All + producer: Producer + shop: Shop + product: Product + variant: Variant + + columns: Columns + actions: Actions + viewing: "Viewing: %{current_view_name}" + + inventory: + sku: SKU + price: Price + on_hand: On Hand + on_demand: On Demand? + enable_reset: Enable Stock Level Reset? + inherit: Inherit? + add: Add + hide: Hide + select_a_shop: Select A Shop + review_now: Review Now + new_products_alert_message: There are %{new_product_count} new products available to add to your inventory. + currently_empty: Your inventory is currently empty + no_matching_products: No matching products found in your inventory + no_hidden_products: No products have been hidden from this inventory + no_matching_hidden_products: No hidden products match your search criteria + no_new_products: No new products are available to add to this inventory + no_matching_new_products: No new products match your search criteria + inventory_powertip: This is your inventory of products. To add products to your inventory, select 'New Products' from the Viewing dropdown. + hidden_powertip: These products have been hidden from your inventory and will not be available to add to your shop. You can click 'Add' to add a product to you inventory. + new_powertip: These products are available to be added to your inventory. Click 'Add' to add a product to your inventory, or 'Hide' to hide it from view. You can always change your mind later! + # Printable Invoice Columns invoice_column_item: "Item" invoice_column_qty: "Qty" From f288c09380858d6fa9a24929d15d36ba5504f726 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 11 Feb 2016 11:21:16 +1100 Subject: [PATCH 37/54] Moving inventory settings to their own section within the enterprise edit form --- .../side_menu_controller.js.coffee | 4 ++++ app/views/admin/enterprises/_form.html.haml | 4 ++++ .../form/_inventory_settings.html.haml | 19 +++++++++++++++++++ .../form/_shop_preferences.html.haml | 14 -------------- 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 app/views/admin/enterprises/form/_inventory_settings.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 45a5d068a4..d464585f67 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 @@ -17,6 +17,7 @@ angular.module("admin.enterprises") { 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: "Inventory Settings", icon_class: "icon-list-ol", show: "showInventorySettings()" } { name: "Shop Preferences", icon_class: "icon-shopping-cart", show: "showShopPreferences()" } ] @@ -41,5 +42,8 @@ angular.module("admin.enterprises") $scope.showEnterpriseFees = -> enterprisePermissions.can_manage_enterprise_fees && ($scope.Enterprise.sells != "none" || $scope.Enterprise.is_primary_producer) + $scope.showInventorySettings = -> + $scope.Enterprise.sells != "none" + $scope.showShopPreferences = -> $scope.Enterprise.sells != "none" diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 28f76d34d0..79faeea229 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -47,6 +47,10 @@ %legend Enterprise Fees = render 'admin/enterprises/form/enterprise_fees', f: f +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Inventory Settings'" } } + %legend Inventory Settings + = render 'admin/enterprises/form/inventory_settings', 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/_inventory_settings.html.haml b/app/views/admin/enterprises/form/_inventory_settings.html.haml new file mode 100644 index 0000000000..021a5aabe4 --- /dev/null +++ b/app/views/admin/enterprises/form/_inventory_settings.html.haml @@ -0,0 +1,19 @@ +-# This is hopefully a temporary measure, pending the arrival of multiple named inventories +-# for shops. We need this here to allow hubs to restrict visible variants to only those in +-# their inventory if they so choose +.row + .alpha.eleven.columns + You can opt to use your + %strong + %a{href: main_app.admin_variant_overrides_path } inventory + to restrict the list of products that are available to add to your shop in the Order Cycle interface. To enable this functionality, select 'Inventory Only' below. +.row + .alpha.eleven.columns + .three.columns.alpha + = f.label "enterprise_preferred_product_selection_from_inventory_only", t(:select_products_for_order_cycle_from) + .three.columns + = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "1", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } + = label :enterprise, :preferred_product_selection_from_inventory_only, "Inventory Only" + .five.columns.omega + = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "0", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } + = label :enterprise, :preferred_product_selection_from_inventory_only, "All Available Products (Ignore Inventory)" diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index f732d98a4c..3085727f85 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -33,17 +33,3 @@ .five.columns.omega = radio_button :enterprise, :preferred_shopfront_order_cycle_order, :orders_close_at, { 'ng-model' => 'Enterprise.preferred_shopfront_order_cycle_order' } = label :enterprise, :preferred_shopfront_order_cycle_order_orders_close_at, "Close Date" - --# This is hopefully a temporary measure, pending the arrival of multiple named inventories --# for shops. We need this here to allow hubs to restrict visible variants to only those in --# their inventory if they so choose -.row - .alpha.eleven.columns - .three.columns.alpha - = f.label "enterprise_preferred_product_selection_from_inventory_only", t(:select_products_for_order_cycle_from) - .three.columns - = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "1", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } - = label :enterprise, :preferred_product_selection_from_inventory_only, "Inventory Only" - .five.columns.omega - = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "0", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } - = label :enterprise, :preferred_product_selection_from_inventory_only, "All Available Products (Ignore Inventory)" From 77e74c56421bbb97cc9241d8a7680c68c052c2f5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 11 Feb 2016 17:35:15 +1100 Subject: [PATCH 38/54] OC Coordinators can opt to restrict products in an order cycle to those in their inventory only --- .../admin/advanced_settings.css.scss | 19 ++++++ .../admin/enterprises_controller.rb | 8 +-- .../admin/order_cycles_controller.rb | 6 +- app/models/order_cycle.rb | 3 + app/models/spree/product_decorator.rb | 7 ++ .../api/admin/exchange_serializer.rb | 45 ++++++++----- .../for_order_cycle/enterprise_serializer.rb | 24 ++++++- .../supplied_product_serializer.rb | 13 +++- .../form/_inventory_settings.html.haml | 2 +- .../order_cycles/_advanced_settings.html.haml | 22 +++++++ ...change_distributed_products_form.html.haml | 5 +- app/views/admin/order_cycles/edit.html.haml | 22 ++++++- config/locales/en.yml | 7 ++ .../admin/order_cycles_controller_spec.rb | 26 +++++++- spec/models/order_cycle_spec.rb | 4 +- spec/models/spree/product_spec.rb | 16 +++++ spec/models/spree/variant_spec.rb | 2 +- .../admin/exchange_serializer_spec.rb | 66 +++++++++++++------ .../enterprise_serializer_spec.rb | 49 +++++++++++--- .../supplied_product_serializer_spec.rb | 38 +++++++++++ 20 files changed, 322 insertions(+), 62 deletions(-) create mode 100644 app/assets/stylesheets/admin/advanced_settings.css.scss create mode 100644 app/views/admin/order_cycles/_advanced_settings.html.haml create mode 100644 spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb diff --git a/app/assets/stylesheets/admin/advanced_settings.css.scss b/app/assets/stylesheets/admin/advanced_settings.css.scss new file mode 100644 index 0000000000..6b48e8ce11 --- /dev/null +++ b/app/assets/stylesheets/admin/advanced_settings.css.scss @@ -0,0 +1,19 @@ +#advanced_settings { + background-color: #eff5fc; + border: 1px solid #cee1f4; + margin-bottom: 20px; + + .row{ + margin: 0px -4px; + + padding: 20px 0px; + + .column.alpha, .columns.alpha { + padding-left: 20px; + } + + .column.omega, .columns.omega { + padding-right: 20px; + } + } +} diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 13d5772385..55eaabd7d8 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -96,7 +96,7 @@ module Admin def for_order_cycle respond_to do |format| format.json do - render json: @collection, each_serializer: Api::Admin::ForOrderCycle::EnterpriseSerializer, spree_current_user: spree_current_user + render json: @collection, each_serializer: Api::Admin::ForOrderCycle::EnterpriseSerializer, order_cycle: @order_cycle, spree_current_user: spree_current_user end end end @@ -138,10 +138,10 @@ module Admin def collection case action when :for_order_cycle - order_cycle = OrderCycle.find_by_id(params[:order_cycle_id]) if params[:order_cycle_id] + @order_cycle = OrderCycle.find_by_id(params[:order_cycle_id]) if params[:order_cycle_id] coordinator = Enterprise.find_by_id(params[:coordinator_id]) if params[:coordinator_id] - order_cycle = OrderCycle.new(coordinator: coordinator) if order_cycle.nil? && coordinator.present? - return OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises + @order_cycle = OrderCycle.new(coordinator: coordinator) if @order_cycle.nil? && coordinator.present? + return OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, @order_cycle).visible_enterprises when :index if spree_current_user.admin? OpenFoodNetwork::Permissions.new(spree_current_user). diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 300a2ca24e..1d5d018522 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -60,8 +60,12 @@ module Admin respond_to do |format| if @order_cycle.update_attributes(params[:order_cycle]) - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go! + unless params[:order_cycle][:incoming_exchanges].nil? && params[:order_cycle][:outgoing_exchanges].nil? + # Only update apply exchange information if it is actually submmitted + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go! + end flash[:notice] = 'Your order cycle has been updated.' if params[:reloading] == '1' + format.html { redirect_to main_app.edit_admin_order_cycle_path(@order_cycle) } format.json { render :json => {:success => true} } else format.json { render :json => {:success => false} } diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 03979220d6..2594c132bf 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -11,6 +11,8 @@ class OrderCycle < ActiveRecord::Base validates_presence_of :name, :coordinator_id + preference :product_selection_from_coordinator_inventory_only, :boolean, default: false + scope :active, lambda { where('order_cycles.orders_open_at <= ? AND order_cycles.orders_close_at >= ?', Time.zone.now, Time.zone.now) } scope :active_or_complete, lambda { where('order_cycles.orders_open_at <= ?', Time.zone.now) } scope :inactive, lambda { where('order_cycles.orders_open_at > ? OR order_cycles.orders_close_at < ?', Time.zone.now, Time.zone.now) } @@ -113,6 +115,7 @@ class OrderCycle < ActiveRecord::Base oc.name = "COPY OF #{oc.name}" oc.orders_open_at = oc.orders_close_at = nil oc.coordinator_fee_ids = self.coordinator_fee_ids + oc.preferred_product_selection_from_coordinator_inventory_only = self.preferred_product_selection_from_coordinator_inventory_only oc.save! self.exchanges.each { |e| e.clone!(oc) } oc.reload diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index b089c9a52b..c7e1cfb68d 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -52,6 +52,13 @@ Spree::Product.class_eval do scope :with_order_cycles_inner, joins(:variants_including_master => {:exchanges => :order_cycle}) + scope :visible_for, lambda { |enterprise| + joins('LEFT OUTER JOIN spree_variants AS o_spree_variants ON (o_spree_variants.product_id = spree_products.id)'). + joins('LEFT OUTER JOIN inventory_items AS o_inventory_items ON (o_spree_variants.id = o_inventory_items.variant_id)'). + where('o_inventory_items.enterprise_id = (?) AND visible = (?)', enterprise, true). + select('DISTINCT spree_products.*') + } + # -- Scopes scope :in_supplier, lambda { |supplier| where(:supplier_id => supplier) } diff --git a/app/serializers/api/admin/exchange_serializer.rb b/app/serializers/api/admin/exchange_serializer.rb index 4ef9b8964f..615d49f695 100644 --- a/app/serializers/api/admin/exchange_serializer.rb +++ b/app/serializers/api/admin/exchange_serializer.rb @@ -4,24 +4,35 @@ class Api::Admin::ExchangeSerializer < ActiveModel::Serializer has_many :enterprise_fees, serializer: Api::Admin::BasicEnterpriseFeeSerializer def variants - permitted = Spree::Variant.where("1=0") - if object.incoming - permitted = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle). - visible_variants_for_incoming_exchanges_from(object.sender) + variants = object.incoming? ? visible_incoming_variants : visible_outgoing_variants + Hash[ object.variants.merge(variants).map { |v| [v.id, true] } ] + end + + private + + def visible_incoming_variants + if object.order_cycle.prefers_product_selection_from_coordinator_inventory_only? + permitted_incoming_variants.visible_for(object.order_cycle.coordinator) else - # This is hopefully a temporary measure, pending the arrival of multiple named inventories - # for shops. We need this here to allow hubs to restrict visible variants to only those in - # their inventory if they so choose - permitted = if object.receiver.prefers_product_selection_from_inventory_only? - OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle) - .visible_variants_for_outgoing_exchanges_to(object.receiver) - .visible_for(object.receiver) - else - OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle) - .visible_variants_for_outgoing_exchanges_to(object.receiver) - .not_hidden_for(object.receiver) - end + permitted_incoming_variants end - Hash[ object.variants.merge(permitted).map { |v| [v.id, true] } ] + end + + def visible_outgoing_variants + if object.receiver.prefers_product_selection_from_inventory_only? + permitted_outgoing_variants.visible_for(object.receiver) + else + permitted_outgoing_variants.not_hidden_for(object.receiver) + end + end + + def permitted_incoming_variants + OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle). + visible_variants_for_incoming_exchanges_from(object.sender) + end + + def permitted_outgoing_variants + OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle) + .visible_variants_for_outgoing_exchanges_to(object.receiver) end end diff --git a/app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb b/app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb index 9dfa680476..8e4dee5f5d 100644 --- a/app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb +++ b/app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb @@ -6,7 +6,11 @@ class Api::Admin::ForOrderCycle::EnterpriseSerializer < ActiveModel::Serializer attributes :is_primary_producer, :is_distributor, :sells def issues_summary_supplier - OpenFoodNetwork::EnterpriseIssueValidator.new(object).issues_summary confirmation_only: true + issues = OpenFoodNetwork::EnterpriseIssueValidator.new(object).issues_summary confirmation_only: true + if issues.nil? && products.empty? + issues = "no products in inventory" + end + issues end def issues_summary_distributor @@ -18,8 +22,22 @@ class Api::Admin::ForOrderCycle::EnterpriseSerializer < ActiveModel::Serializer end def supplied_products - objects = object.supplied_products.not_deleted serializer = Api::Admin::ForOrderCycle::SuppliedProductSerializer - ActiveModel::ArraySerializer.new(objects, each_serializer: serializer) + ActiveModel::ArraySerializer.new(products, each_serializer: serializer, order_cycle: order_cycle) + end + + private + + def products + return @products unless @products.nil? + @products = if order_cycle.prefers_product_selection_from_coordinator_inventory_only? + object.supplied_products.not_deleted.visible_for(order_cycle.coordinator) + else + object.supplied_products.not_deleted + end + end + + def order_cycle + options[:order_cycle] end end diff --git a/app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb b/app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb index dbe3ec7a48..054fe44751 100644 --- a/app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb +++ b/app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb @@ -14,8 +14,17 @@ class Api::Admin::ForOrderCycle::SuppliedProductSerializer < ActiveModel::Serial end def variants - object.variants.map do |variant| - { id: variant.id, label: variant.full_name } + variants = if order_cycle.prefers_product_selection_from_coordinator_inventory_only? + object.variants.visible_for(order_cycle.coordinator) + else + object.variants end + variants.map { |variant| { id: variant.id, label: variant.full_name } } + end + + private + + def order_cycle + options[:order_cycle] end end diff --git a/app/views/admin/enterprises/form/_inventory_settings.html.haml b/app/views/admin/enterprises/form/_inventory_settings.html.haml index 021a5aabe4..ded4f6d497 100644 --- a/app/views/admin/enterprises/form/_inventory_settings.html.haml +++ b/app/views/admin/enterprises/form/_inventory_settings.html.haml @@ -10,7 +10,7 @@ .row .alpha.eleven.columns .three.columns.alpha - = f.label "enterprise_preferred_product_selection_from_inventory_only", t(:select_products_for_order_cycle_from) + = f.label "enterprise_preferred_product_selection_from_inventory_only", t('admin.enterprise.select_outgoing_oc_products_from') .three.columns = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "1", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } = label :enterprise, :preferred_product_selection_from_inventory_only, "Inventory Only" diff --git a/app/views/admin/order_cycles/_advanced_settings.html.haml b/app/views/admin/order_cycles/_advanced_settings.html.haml new file mode 100644 index 0000000000..27b347880b --- /dev/null +++ b/app/views/admin/order_cycles/_advanced_settings.html.haml @@ -0,0 +1,22 @@ +.row + .alpha.omega.sixteen.columns + %h3 Advanced Settings + += form_for [main_app, :admin, @order_cycle] do |f| + .row + .six.columns.alpha + = f.label "enterprise_preferred_product_selection_from_coordinator_inventory_only", t('admin.order_cycle.choose_products_from') + .with-tip{'data-powertip' => "You can opt to restrict all available products (both incoming and outgoing), to only those in #{@order_cycle.coordinator.name}'s inventory."} + %a What's this? + .four.columns + = f.radio_button :preferred_product_selection_from_coordinator_inventory_only, true + = f.label :preferred_product_selection_from_coordinator_inventory_only, "Coordinator's Inventory Only" + .six.columns.omega + = f.radio_button :preferred_product_selection_from_coordinator_inventory_only, false + = f.label :preferred_product_selection_from_coordinator_inventory_only, "All Available Products" + + .row + .sixteen.columns.alpha.omega.text-center + %input{ type: 'submit', value: 'Save and Reload Page' } + or + %a{ href: "#", onClick: "toggleSettings()" } Close 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 cff8f5d2b3..e2218599ce 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 @@ -9,8 +9,9 @@ .exchange-product{'ng-repeat' => 'product in supplied_products | filter:productSuppliedToOrderCycle | visibleProducts:exchange:order_cycle.visible_variants_for_outgoing_exchanges | orderBy:"name"' } .exchange-product-details %label - = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', - 'ng-disabled' => 'product.variants.length > 0 || !order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(product.master_id) < 0' + -# MASTER_VARIANTS: No longer required + -# = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', + -# 'ng-disabled' => 'product.variants.length > 0 || !order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(product.master_id) < 0' %img{'ng-src' => '{{ product.image_url }}'} .name {{ product.name }} .supplier {{ product.supplier_name }} diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index 12dc6238ae..d2b7605289 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -1,9 +1,27 @@ -- if can? :notify_producers, @order_cycle - = content_for :page_actions do +- content_for :page_actions do + :javascript + function toggleSettings(){ + if( $('#advanced_settings').is(":visible") ){ + $('button#toggle_settings i').switchClass("icon-chevron-up","icon-chevron-down") + } + else { + $('button#toggle_settings i').switchClass("icon-chevron-down","icon-chevron-up") + } + $("#advanced_settings").slideToggle() + } + + - if can? :notify_producers, @order_cycle %li = button_to "Notify producers", main_app.notify_producers_admin_order_cycle_path, :id => 'admin_notify_producers', :confirm => 'Are you sure?' + %li + %button#toggle_settings{ onClick: 'toggleSettings()' } + Advanced Settings + %i.icon-chevron-down +#advanced_settings{ hidden: true } + = render partial: "/admin/order_cycles/advanced_settings" + %h1 Edit Order Cycle diff --git a/config/locales/en.yml b/config/locales/en.yml index e5c0983ac2..a72afe655d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -94,6 +94,13 @@ en: hidden_powertip: These products have been hidden from your inventory and will not be available to add to your shop. You can click 'Add' to add a product to you inventory. new_powertip: These products are available to be added to your inventory. Click 'Add' to add a product to your inventory, or 'Hide' to hide it from view. You can always change your mind later! + + order_cycle: + choose_products_from: "Choose Products From:" + + enterprise: + select_outgoing_oc_products_from: Select outgoing OC products from + # Printable Invoice Columns invoice_column_item: "Item" invoice_column_qty: "Qty" diff --git a/spec/controllers/admin/order_cycles_controller_spec.rb b/spec/controllers/admin/order_cycles_controller_spec.rb index 9fc9ad9096..3cc29193ab 100644 --- a/spec/controllers/admin/order_cycles_controller_spec.rb +++ b/spec/controllers/admin/order_cycles_controller_spec.rb @@ -103,10 +103,34 @@ module Admin end it "does not set flash message otherwise" do - spree_put :update, id: order_cycle.id, reloading: '0', order_cycle: {} flash[:notice].should be_nil end + context "when updating without explicitly submitting exchanges" do + let(:form_applicator_mock) { double(:form_applicator) } + let(:incoming_exchange) { create(:exchange, order_cycle: order_cycle, incoming: true) } + let(:outgoing_exchange) { create(:exchange, order_cycle: order_cycle, incoming: false) } + + + before do + allow(OpenFoodNetwork::OrderCycleFormApplicator).to receive(:new) { form_applicator_mock } + allow(form_applicator_mock).to receive(:go!) { nil } + end + + it "does not run the OrderCycleFormApplicator" do + expect(order_cycle.exchanges.incoming).to eq [incoming_exchange] + expect(order_cycle.exchanges.outgoing).to eq [outgoing_exchange] + expect(order_cycle.prefers_product_selection_from_coordinator_inventory_only?).to be false + spree_put :update, id: order_cycle.id, order_cycle: { name: 'Some new name', preferred_product_selection_from_coordinator_inventory_only: true } + expect(form_applicator_mock).to_not have_received(:go!) + order_cycle.reload + expect(order_cycle.exchanges.incoming).to eq [incoming_exchange] + expect(order_cycle.exchanges.outgoing).to eq [outgoing_exchange] + expect(order_cycle.name).to eq 'Some new name' + expect(order_cycle.prefers_product_selection_from_coordinator_inventory_only?).to be true + end + end + context "as a producer supplying to an order cycle" do let(:producer) { create(:supplier_enterprise) } let(:coordinator) { order_cycle.coordinator } diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index 90465fb80e..c8c76abb1d 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -380,7 +380,7 @@ describe OrderCycle do it "clones itself" do coordinator = create(:enterprise); - oc = create(:simple_order_cycle, coordinator_fees: [create(:enterprise_fee, enterprise: coordinator)]) + oc = create(:simple_order_cycle, coordinator_fees: [create(:enterprise_fee, enterprise: coordinator)], preferred_product_selection_from_coordinator_inventory_only: true) ex1 = create(:exchange, order_cycle: oc) ex2 = create(:exchange, order_cycle: oc) oc.clone! @@ -390,10 +390,12 @@ describe OrderCycle do occ.orders_open_at.should be_nil occ.orders_close_at.should be_nil occ.coordinator.should_not be_nil + occ.preferred_product_selection_from_coordinator_inventory_only.should be_true occ.coordinator.should == oc.coordinator occ.coordinator_fee_ids.should_not be_empty occ.coordinator_fee_ids.should == oc.coordinator_fee_ids + occ.preferred_product_selection_from_coordinator_inventory_only.should == oc.preferred_product_selection_from_coordinator_inventory_only # to_h gives us a unique hash for each exchange # check that the clone has no additional exchanges diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 25913f679b..c37b49bc23 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -338,6 +338,22 @@ module Spree product.should include @p2 end end + + describe "visible_for" do + let(:enterprise) { create(:distributor_enterprise) } + let!(:new_variant) { create(:variant) } + let!(:hidden_variant) { create(:variant) } + let!(:visible_variant) { create(:variant) } + let!(:hidden_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: hidden_variant, visible: false ) } + let!(:visible_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: visible_variant, visible: true ) } + + let!(:products) { Spree::Product.visible_for(enterprise) } + + it "lists any products with variants that are listed as visible=true" do + expect(products).to include visible_variant.product + expect(products).to_not include new_variant.product, hidden_variant.product + end + end end describe "finders" do diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 1f5aeeba18..306381630f 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -133,7 +133,7 @@ module Spree context "finding variants that are visible in an enterprise's inventory" do let!(:variants) { Spree::Variant.visible_for(enterprise) } - it "lists any variants that are not listed as visible=false" do + it "lists any variants that are listed as visible=true" do expect(variants).to include visible_variant expect(variants).to_not include new_variant, hidden_variant end diff --git a/spec/serializers/admin/exchange_serializer_spec.rb b/spec/serializers/admin/exchange_serializer_spec.rb index ecb85d07d8..649a0b427e 100644 --- a/spec/serializers/admin/exchange_serializer_spec.rb +++ b/spec/serializers/admin/exchange_serializer_spec.rb @@ -3,65 +3,93 @@ require 'open_food_network/order_cycle_permissions' describe Api::Admin::ExchangeSerializer do let(:v1) { create(:variant) } let(:v2) { create(:variant) } + let(:v3) { create(:variant) } let(:permissions_mock) { double(:permissions) } + let(:permitted_variants) { Spree::Variant.where(id: [v1, v2]) } let(:serializer) { Api::Admin::ExchangeSerializer.new exchange } + context "serializing incoming exchanges" do - let(:exchange) { create(:exchange, incoming: true, variants: [v1, v2]) } - let(:permitted_variants) { double(:permitted_variants) } + let(:exchange) { create(:exchange, incoming: true, variants: [v1, v2, v3]) } + let!(:inventory_item) { create(:inventory_item, enterprise: exchange.order_cycle.coordinator, variant: v1, visible: true) } before do allow(OpenFoodNetwork::OrderCyclePermissions).to receive(:new) { permissions_mock } - allow(permissions_mock).to receive(:visible_variants_for_incoming_exchanges_from) { Spree::Variant.where(id: [v1]) } + allow(permissions_mock).to receive(:visible_variants_for_incoming_exchanges_from) { permitted_variants } + allow(permitted_variants).to receive(:visible_for).and_call_original end - it "filters variants within the exchange based on permissions" do - visible_variants = serializer.variants - expect(permissions_mock).to have_received(:visible_variants_for_incoming_exchanges_from).with(exchange.sender) - expect(exchange.variants).to include v1, v2 - expect(visible_variants.keys).to include v1.id - expect(visible_variants.keys).to_not include v2.id + context "when order cycle shows only variants in the coordinator's inventory" do + before do + allow(exchange.order_cycle).to receive(:prefers_product_selection_from_coordinator_inventory_only?) { true } + end + + it "filters variants within the exchange based on permissions, and visibility in inventory" do + visible_variants = serializer.variants + expect(permissions_mock).to have_received(:visible_variants_for_incoming_exchanges_from).with(exchange.sender) + expect(permitted_variants).to have_received(:visible_for).with(exchange.order_cycle.coordinator) + expect(exchange.variants).to include v1, v2, v3 + expect(visible_variants.keys).to include v1.id + expect(visible_variants.keys).to_not include v2.id, v3.id + end + end + + context "when order cycle shows all available products" do + before do + allow(exchange.order_cycle).to receive(:prefers_product_selection_from_coordinator_inventory_only?) { false } + end + + it "filters variants within the exchange based on permissions only" do + visible_variants = serializer.variants + expect(permissions_mock).to have_received(:visible_variants_for_incoming_exchanges_from).with(exchange.sender) + expect(permitted_variants).to_not have_received(:visible_for) + expect(exchange.variants).to include v1, v2, v3 + expect(visible_variants.keys).to include v1.id, v2.id + expect(visible_variants.keys).to_not include v3.id + end end end context "serializing outgoing exchanges" do - let(:exchange) { create(:exchange, incoming: false, variants: [v1, v2]) } - let(:permitted_variants) { double(:permitted_variants) } + let(:exchange) { create(:exchange, incoming: false, variants: [v1, v2, v3]) } + let!(:inventory_item) { create(:inventory_item, enterprise: exchange.receiver, variant: v1, visible: true) } before do allow(OpenFoodNetwork::OrderCyclePermissions).to receive(:new) { permissions_mock } allow(permissions_mock).to receive(:visible_variants_for_outgoing_exchanges_to) { permitted_variants } + allow(permitted_variants).to receive(:visible_for).and_call_original + allow(permitted_variants).to receive(:not_hidden_for).and_call_original end context "when the receiver prefers to see all variants (not just those in their inventory)" do before do allow(exchange.receiver).to receive(:prefers_product_selection_from_inventory_only?) { false } - allow(permitted_variants).to receive(:not_hidden_for) { Spree::Variant.where(id: [v1]) } end - it "filters variants within the exchange based on permissions" do + it "filters variants within the exchange based on permissions only" do visible_variants = serializer.variants expect(permissions_mock).to have_received(:visible_variants_for_outgoing_exchanges_to).with(exchange.receiver) expect(permitted_variants).to have_received(:not_hidden_for).with(exchange.receiver) - expect(exchange.variants).to include v1, v2 - expect(visible_variants.keys).to include v1.id - expect(visible_variants.keys).to_not include v2.id + expect(permitted_variants).to_not have_received(:visible_for) + expect(exchange.variants).to include v1, v2, v3 + expect(visible_variants.keys).to include v1.id, v2.id + expect(visible_variants.keys).to_not include v3.id end end context "when the receiver prefers to restrict visible variants to only those in their inventory" do before do allow(exchange.receiver).to receive(:prefers_product_selection_from_inventory_only?) { true } - allow(permitted_variants).to receive(:visible_for) { Spree::Variant.where(id: [v1]) } end it "filters variants within the exchange based on permissions, and inventory visibility" do visible_variants = serializer.variants expect(permissions_mock).to have_received(:visible_variants_for_outgoing_exchanges_to).with(exchange.receiver) expect(permitted_variants).to have_received(:visible_for).with(exchange.receiver) - expect(exchange.variants).to include v1, v2 + expect(permitted_variants).to_not have_received(:not_hidden_for) + expect(exchange.variants).to include v1, v2, v3 expect(visible_variants.keys).to include v1.id - expect(visible_variants.keys).to_not include v2.id + expect(visible_variants.keys).to_not include v2.id, v3.id end end end diff --git a/spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb b/spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb index db476164eb..c30b6374c9 100644 --- a/spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb +++ b/spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb @@ -1,13 +1,46 @@ describe Api::Admin::ForOrderCycle::EnterpriseSerializer do - let(:enterprise) { create(:distributor_enterprise) } - let!(:product) { create(:simple_product, supplier: enterprise) } - let!(:deleted_product) { create(:simple_product, supplier: enterprise, deleted_at: 24.hours.ago ) } - let(:serialized_enterprise) { Api::Admin::ForOrderCycle::EnterpriseSerializer.new(enterprise, spree_current_user: enterprise.owner ).to_json } + let(:coordinator) { create(:distributor_enterprise) } + let(:order_cycle) { double(:order_cycle, coordinator: coordinator) } + let(:enterprise) { create(:distributor_enterprise) } + let!(:non_inventory_product) { create(:simple_product, supplier: enterprise) } + let!(:non_inventory_variant) { non_inventory_product.variants.first } + let!(:inventory_product) { create(:simple_product, supplier: enterprise) } + let!(:inventory_variant) { inventory_product.variants.first } + let!(:deleted_product) { create(:simple_product, supplier: enterprise, deleted_at: 24.hours.ago ) } + let!(:deleted_variant) { deleted_product.variants.first } + let(:serialized_enterprise) { Api::Admin::ForOrderCycle::EnterpriseSerializer.new(enterprise, order_cycle: order_cycle, spree_current_user: enterprise.owner ).to_json } + let!(:inventory_item1) { create(:inventory_item, enterprise: coordinator, variant: inventory_variant, visible: true)} + let!(:inventory_item2) { create(:inventory_item, enterprise: coordinator, variant: deleted_variant, visible: true)} - describe "supplied products" do - it "does not render deleted products" do - expect(serialized_enterprise).to have_json_size(1).at_path 'supplied_products' - expect(serialized_enterprise).to be_json_eql(product.master.id).at_path 'supplied_products/0/master_id' + context "when order cycle shows only variants in the coordinator's inventory" do + before do + allow(order_cycle).to receive(:prefers_product_selection_from_coordinator_inventory_only?) { true } + end + + describe "supplied products" do + it "renders only non-deleted variants that are in the coordinators inventory" do + expect(serialized_enterprise).to have_json_size(1).at_path 'supplied_products' + expect(serialized_enterprise).to have_json_size(1).at_path 'supplied_products/0/variants' + expect(serialized_enterprise).to be_json_eql(inventory_variant.id).at_path 'supplied_products/0/variants/0/id' + end end end + + + context "when order cycle shows all available products" do + before do + allow(order_cycle).to receive(:prefers_product_selection_from_coordinator_inventory_only?) { false } + end + + describe "supplied products" do + it "renders variants that are not in the coordinators inventory but not variants of deleted products" do + expect(serialized_enterprise).to have_json_size(2).at_path 'supplied_products' + expect(serialized_enterprise).to have_json_size(1).at_path 'supplied_products/0/variants' + expect(serialized_enterprise).to have_json_size(1).at_path 'supplied_products/1/variants' + variant_ids = parse_json(serialized_enterprise)['supplied_products'].map{ |p| p['variants'].first['id'] } + expect(variant_ids).to include non_inventory_variant.id, inventory_variant.id + end + end + end + end diff --git a/spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb b/spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb new file mode 100644 index 0000000000..16c562120e --- /dev/null +++ b/spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb @@ -0,0 +1,38 @@ +describe Api::Admin::ForOrderCycle::EnterpriseSerializer do + let(:coordinator) { create(:distributor_enterprise) } + let(:order_cycle) { double(:order_cycle, coordinator: coordinator) } + let!(:product) { create(:simple_product) } + let!(:non_inventory_variant) { product.variants.first } + let!(:inventory_variant) { create(:variant, product: product.reload) } + let(:serialized_product) { Api::Admin::ForOrderCycle::SuppliedProductSerializer.new(product, order_cycle: order_cycle ).to_json } + let!(:inventory_item) { create(:inventory_item, enterprise: coordinator, variant: inventory_variant, visible: true)} + + context "when order cycle shows only variants in the coordinator's inventory" do + before do + allow(order_cycle).to receive(:prefers_product_selection_from_coordinator_inventory_only?) { true } + end + + describe "variants" do + it "renders only variants that are in the coordinators inventory" do + expect(serialized_product).to have_json_size(1).at_path 'variants' + expect(serialized_product).to be_json_eql(inventory_variant.id).at_path 'variants/0/id' + end + end + end + + + context "when order cycle shows all available products" do + before do + allow(order_cycle).to receive(:prefers_product_selection_from_coordinator_inventory_only?) { false } + end + + describe "supplied products" do + it "renders variants regardless of whether they are in the coordinators inventory" do + expect(serialized_product).to have_json_size(2).at_path 'variants' + variant_ids = parse_json(serialized_product)['variants'].map{ |v| v['id'] } + expect(variant_ids).to include non_inventory_variant.id, inventory_variant.id + end + end + end + +end From e16ca82e76e491612e5b96c6eb0538c7753d7a0b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 15 Jan 2016 13:29:28 +1100 Subject: [PATCH 39/54] Extract product JSON rendering to lib class. Fix HTML stripping that never actually worked. --- app/controllers/shop_controller.rb | 72 ++------------ app/serializers/api/product_serializer.rb | 5 + lib/open_food_network/products_renderer.rb | 84 +++++++++++++++++ spec/controllers/shop_controller_spec.rb | 93 +------------------ .../products_renderer_spec.rb | 91 ++++++++++++++++++ 5 files changed, 188 insertions(+), 157 deletions(-) create mode 100644 lib/open_food_network/products_renderer.rb create mode 100644 spec/lib/open_food_network/products_renderer_spec.rb diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index ef78605b41..8d7b9ab274 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -1,4 +1,4 @@ -require 'open_food_network/scope_product_to_hub' +require 'open_food_network/products_renderer' class ShopController < BaseController layout "darkswarm" @@ -10,22 +10,13 @@ class ShopController < BaseController end def products - if @products = products_for_shop + begin + products_json = OpenFoodNetwork::ProductsRenderer.new(current_distributor, current_order_cycle).products - enterprise_fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new current_distributor, current_order_cycle + render json: products_json - render status: 200, - json: ActiveModel::ArraySerializer.new(@products, - each_serializer: Api::ProductSerializer, - current_order_cycle: current_order_cycle, - current_distributor: current_distributor, - variants: variants_for_shop_by_id, - master_variants: master_variants_for_shop_by_id, - enterprise_fee_calculator: enterprise_fee_calculator, - ).to_json - - else - render json: "", status: 404 + rescue OpenFoodNetwork::ProductsRenderer::NoProducts + render status: 404, json: '' end end @@ -42,55 +33,4 @@ class ShopController < BaseController end end - private - - def products_for_shop - if current_order_cycle - scoper = OpenFoodNetwork::ScopeProductToHub.new(current_distributor) - - current_order_cycle. - valid_products_distributed_by(current_distributor). - order(taxon_order). - each { |p| scoper.scope(p) }. - select { |p| !p.deleted? && p.has_stock_for_distribution?(current_order_cycle, current_distributor) } - end - 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 - - def all_variants_for_shop - # 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. - scoper = OpenFoodNetwork::ScopeVariantToHub.new(current_distributor) - Spree::Variant. - for_distribution(current_order_cycle, current_distributor). - each { |v| scoper.scope(v) }. - select(&:in_stock?) - end - - def variants_for_shop_by_id - index_by_product_id all_variants_for_shop.reject(&:is_master) - end - - def master_variants_for_shop_by_id - index_by_product_id all_variants_for_shop.select(&:is_master) - end - - def index_by_product_id(variants) - variants.inject({}) do |vs, v| - vs[v.product_id] ||= [] - vs[v.product_id] << v - vs - end - end end diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index 5a1d1b5c86..03a66afe89 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -34,6 +34,7 @@ end class Api::CachedProductSerializer < ActiveModel::Serializer #cached #delegate :cache_key, to: :object + include ActionView::Helpers::SanitizeHelper attributes :id, :name, :permalink, :count_on_hand attributes :on_demand, :group_buy, :notes, :description @@ -48,6 +49,10 @@ class Api::CachedProductSerializer < ActiveModel::Serializer has_many :images, serializer: Api::ImageSerializer has_one :supplier, serializer: Api::IdSerializer + def description + strip_tags object.description + end + def properties_with_values object.properties_including_inherited end diff --git a/lib/open_food_network/products_renderer.rb b/lib/open_food_network/products_renderer.rb new file mode 100644 index 0000000000..d745b1b794 --- /dev/null +++ b/lib/open_food_network/products_renderer.rb @@ -0,0 +1,84 @@ +require 'open_food_network/scope_product_to_hub' + +module OpenFoodNetwork + class ProductsRenderer + class NoProducts < Exception; end + + def initialize(distributor, order_cycle) + @distributor = distributor + @order_cycle = order_cycle + end + + def products + products = products_for_shop + + if products + enterprise_fee_calculator = EnterpriseFeeCalculator.new @distributor, @order_cycle + + ActiveModel::ArraySerializer.new(products, + each_serializer: Api::ProductSerializer, + current_order_cycle: @order_cycle, + current_distributor: @distributor, + variants: variants_for_shop_by_id, + master_variants: master_variants_for_shop_by_id, + enterprise_fee_calculator: enterprise_fee_calculator, + ).to_json + else + raise NoProducts.new + end + end + + + private + + def products_for_shop + if @order_cycle + scoper = ScopeProductToHub.new(@distributor) + + @order_cycle. + valid_products_distributed_by(@distributor). + order(taxon_order). + each { |p| scoper.scope(p) }. + select { |p| !p.deleted? && p.has_stock_for_distribution?(@order_cycle, @distributor) } + end + end + + def taxon_order + if @distributor.preferred_shopfront_taxon_order.present? + @distributor + .preferred_shopfront_taxon_order + .split(",").map { |id| "primary_taxon_id=#{id} DESC" } + .join(",") + ", name ASC" + else + "name ASC" + end + end + + def all_variants_for_shop + # 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. + scoper = OpenFoodNetwork::ScopeVariantToHub.new(@distributor) + Spree::Variant. + for_distribution(@order_cycle, @distributor). + each { |v| scoper.scope(v) }. + select(&:in_stock?) + end + + def variants_for_shop_by_id + index_by_product_id all_variants_for_shop.reject(&:is_master) + end + + def master_variants_for_shop_by_id + index_by_product_id all_variants_for_shop.select(&:is_master) + end + + def index_by_product_id(variants) + variants.inject({}) do |vs, v| + vs[v.product_id] ||= [] + vs[v.product_id] << v + vs + end + end + end +end diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 2c7105b356..09ea85dd44 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -37,7 +37,7 @@ describe ShopController do controller.current_order_cycle.should == oc2 end - context "RABL tests" do + context "JSON tests" do render_views it "should return the order cycle details when the oc is selected" do oc1 = create(:simple_order_cycle, distributors: [d]) @@ -86,7 +86,7 @@ describe ShopController do describe "requests and responses" do let(:product) { create(:product) } before do - exchange.variants << product.master + exchange.variants << product.variants.first end it "returns products via json" do @@ -102,95 +102,6 @@ describe ShopController do response.body.should be_empty end 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) } - - before do - exchange.variants << p1.variants.first - exchange.variants << p2.variants.first - exchange.variants << p3.variants.first - exchange.variants << p4.variants.first - 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 "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) } - let(:variant) { product.variants.first } - - before do - exchange.variants << variant - controller.stub(:current_order_cycle).and_return order_cycle - end - - it "only returns products for the current order cycle" do - xhr :get, :products - response.body.should have_content product.name - end - - it "doesn't return products not in stock" do - variant.update_attribute(:count_on_hand, 0) - xhr :get, :products - response.body.should_not have_content product.name - end - - it "strips html from description" do - product.update_attribute(:description, "turtles frogs") - xhr :get, :products - response.body.should have_content "frogs" - response.body.should_not have_content " [v1]} end end end diff --git a/spec/lib/open_food_network/products_renderer_spec.rb b/spec/lib/open_food_network/products_renderer_spec.rb new file mode 100644 index 0000000000..875eba1ac9 --- /dev/null +++ b/spec/lib/open_food_network/products_renderer_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' +require 'open_food_network/products_renderer' + +module OpenFoodNetwork + describe ProductsRenderer do + let(:d) { 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) } + let(:pr) { ProductsRenderer.new(d, order_cycle) } + + 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) } + + before do + exchange.variants << p1.variants.first + exchange.variants << p2.variants.first + exchange.variants << p3.variants.first + exchange.variants << p4.variants.first + end + + it "sorts products by the distributor's preferred taxon list" do + d.stub(:preferred_shopfront_taxon_order) {"#{t1.id},#{t2.id}"} + products = pr.send(:products_for_shop) + products.should == [p2, p4, p1, p3] + end + + it "alphabetizes products by name when taxon list is not set" do + d.stub(:preferred_shopfront_taxon_order) {""} + products = pr.send(:products_for_shop) + products.should == [p1, p2, p3, p4] + end + end + + context "JSON tests" do + let(:product) { create(:product) } + let(:variant) { product.variants.first } + + before do + exchange.variants << variant + end + + it "only returns products for the current order cycle" do + pr.products.should include product.name + end + + it "doesn't return products not in stock" do + variant.update_attribute(:count_on_hand, 0) + pr.products.should_not include product.name + end + + it "strips html from description" do + product.update_attribute(:description, "turtles frogs") + json = pr.products + json.should include "frogs" + json.should_not include " [v1]} + end + end + end +end From f280b96215ac599a990f754460306a2af61eb933 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 12 Feb 2016 12:26:53 +1100 Subject: [PATCH 40/54] Hiding a variant from inventory prevents it being available on the shopfront User preferences around inventory-only product selection immediately affect the shopfront --- Gemfile.lock | 3 - app/models/enterprise.rb | 8 ++ app/models/order_cycle.rb | 3 + .../products_renderer_spec.rb | 33 ++++++- spec/models/order_cycle_spec.rb | 93 ++++++++++++------- 5 files changed, 98 insertions(+), 42 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 75763fb4a0..6dd9888f8c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -730,6 +730,3 @@ DEPENDENCIES whenever wicked_pdf wkhtmltopdf-binary - -BUNDLED WITH - 1.10.6 diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 162c4c8e29..9979f06c0d 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -253,6 +253,14 @@ class Enterprise < ActiveRecord::Base strip_url read_attribute(:linkedin) end + def inventory_variants + if prefers_product_selection_from_inventory_only? + Spree::Variant.visible_for(self) + else + Spree::Variant.not_hidden_for(self) + end + end + def distributed_variants Spree::Variant.joins(:product).merge(Spree::Product.in_distributor(self)).select('spree_variants.*') end diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 2594c132bf..31a68faa37 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -145,12 +145,15 @@ class OrderCycle < ActiveRecord::Base end def distributed_variants + # TODO: only used in DistributionChangeValidator, can we remove? self.exchanges.outgoing.map(&:variants).flatten.uniq.reject(&:deleted?) end def variants_distributed_by(distributor) + return Spree::Variant.where("1=0") unless distributor.present? Spree::Variant. not_deleted. + merge(distributor.inventory_variants). joins(:exchanges). merge(Exchange.in_order_cycle(self)). merge(Exchange.outgoing). diff --git a/spec/lib/open_food_network/products_renderer_spec.rb b/spec/lib/open_food_network/products_renderer_spec.rb index 875eba1ac9..a231fd3688 100644 --- a/spec/lib/open_food_network/products_renderer_spec.rb +++ b/spec/lib/open_food_network/products_renderer_spec.rb @@ -77,14 +77,37 @@ module OpenFoodNetwork describe "loading variants" do let(:hub) { create(:distributor_enterprise) } - let(:oc) { create(:simple_order_cycle, distributors: [hub], variants: [v1]) } + let(:oc) { create(:simple_order_cycle, distributors: [hub], variants: [v1, v3, v4]) } let(:p) { create(:simple_product) } - let!(:v1) { create(:variant, product: p, unit_value: 3) } - let!(:v2) { create(:variant, product: p, unit_value: 5) } + let!(:v1) { create(:variant, product: p, unit_value: 3) } # In exchange, not in inventory (ie. not_hidden) + let!(:v2) { create(:variant, product: p, unit_value: 5) } # Not in exchange + let!(:v3) { create(:variant, product: p, unit_value: 7, inventory_items: [create(:inventory_item, enterprise: hub, visible: true)]) } + let!(:v4) { create(:variant, product: p, unit_value: 9, inventory_items: [create(:inventory_item, enterprise: hub, visible: false)]) } + let(:pr) { ProductsRenderer.new(hub, oc) } + let(:variants) { pr.send(:variants_for_shop_by_id) } it "scopes variants to distribution" do - pr = ProductsRenderer.new(hub, oc) - pr.send(:variants_for_shop_by_id).should == {p.id => [v1]} + expect(variants[p.id]).to include v1 + expect(variants[p.id]).to_not include v2 + end + + it "does not render variants that have been hidden by the hub" do + # but does render 'new' variants, ie. v1 + expect(variants[p.id]).to include v1, v3 + expect(variants[p.id]).to_not include v4 + end + + + context "when hub opts to only see variants in its inventory" do + before do + allow(hub).to receive(:prefers_product_selection_from_inventory_only?) { true } + end + + it "does not render variants that have not been explicitly added to the inventory for the hub" do + # but does render 'new' variants, ie. v1 + expect(variants[p.id]).to include v3 + expect(variants[p.id]).to_not include v1, v4 + end end end end diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index c8c76abb1d..1663d5dea6 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -178,59 +178,84 @@ describe OrderCycle do end describe "product exchanges" do + let(:oc) { create(:simple_order_cycle) } + let(:d1) { create(:enterprise) } + let(:d2) { create(:enterprise) } + let!(:e0) { create(:exchange, incoming: true, + order_cycle: oc, sender: create(:enterprise), receiver: oc.coordinator) } + let!(:e1) { create(:exchange, incoming: false, + order_cycle: oc, sender: oc.coordinator, receiver: d1) } + let!(:e2) { create(:exchange, incoming: false, + order_cycle: oc, sender: oc.coordinator, receiver: d2) } + let!(:p0) { create(:simple_product) } + let!(:p1) { create(:simple_product) } + let!(:p1_v_deleted) { create(:variant, product: p1, deleted_at: Time.zone.now) } + let!(:p1_v_visible) { create(:variant, product: p1, inventory_items: [create(:inventory_item, enterprise: d2, visible: true)]) } + let!(:p1_v_hidden) { create(:variant, product: p1, inventory_items: [create(:inventory_item, enterprise: d2, visible: false)]) } + let!(:p2) { create(:simple_product) } + let!(:p2_v) { create(:variant, product: p2) } + before(:each) do - @oc = create(:simple_order_cycle) - - @d1 = create(:enterprise) - @d2 = create(:enterprise) - - @e0 = create(:exchange, incoming: true, - order_cycle: @oc, sender: create(:enterprise), receiver: @oc.coordinator) - @e1 = create(:exchange, incoming: false, - order_cycle: @oc, sender: @oc.coordinator, receiver: @d1) - @e2 = create(:exchange, incoming: false, - order_cycle: @oc, sender: @oc.coordinator, receiver: @d2) - - @p0 = create(:simple_product) - @p1 = create(:simple_product) - @p1_v_deleted = create(:variant, product: @p1, deleted_at: Time.zone.now) - @p2 = create(:simple_product) - @p2_v = create(:variant, product: @p2) - - @e0.variants << @p0.master - @e1.variants << @p1.master - @e1.variants << @p2.master - @e1.variants << @p2_v - @e2.variants << @p1.master - @e2.variants << @p1_v_deleted + e0.variants << p0.master + e1.variants << p1.master + e1.variants << p2.master + e1.variants << p2_v + e2.variants << p1.master + e2.variants << p1_v_deleted + e2.variants << p1_v_visible + e2.variants << p1_v_hidden end it "reports on the variants exchanged" do - @oc.variants.should match_array [@p0.master, @p1.master, @p2.master, @p2_v] + oc.variants.should match_array [p0.master, p1.master, p2.master, p2_v, p1_v_visible, p1_v_hidden] end it "returns the correct count of variants" do - @oc.variants.count.should == 4 + oc.variants.count.should == 6 end it "reports on the variants supplied" do - @oc.supplied_variants.should match_array [@p0.master] + oc.supplied_variants.should match_array [p0.master] end it "reports on the variants distributed" do - @oc.distributed_variants.should match_array [@p1.master, @p2.master, @p2_v] - end - - it "reports on the variants distributed by a particular distributor" do - @oc.variants_distributed_by(@d2).should == [@p1.master] + oc.distributed_variants.should match_array [p1.master, p2.master, p2_v, p1_v_visible, p1_v_hidden] end it "reports on the products distributed by a particular distributor" do - @oc.products_distributed_by(@d2).should == [@p1] + oc.products_distributed_by(d2).should == [p1] end it "reports on the products exchanged" do - @oc.products.should match_array [@p0, @p1, @p2] + oc.products.should match_array [p0, p1, p2] + end + + context "listing variant distributed by a particular distributor" do + context "when default settings are in play" do + it "returns an empty list when no distributor is given" do + oc.variants_distributed_by(nil).should == [] + end + + it "returns all variants in the outgoing exchange for the distributor provided" do + oc.variants_distributed_by(d2).should include p1.master, p1_v_visible + oc.variants_distributed_by(d2).should_not include p1_v_hidden, p1_v_deleted + oc.variants_distributed_by(d1).should include p2_v + end + end + + context "when hub prefers product selection from inventory only" do + before do + allow(d1).to receive(:prefers_product_selection_from_inventory_only?) { true } + end + + it "returns an empty list when no distributor is given" do + oc.variants_distributed_by(nil).should == [] + end + + it "returns only variants in the exchange that are also in the distributor's inventory" do + oc.variants_distributed_by(d1).should_not include p2_v + end + end end end From 6ee4e4190e1714ec488a26b7d9e95c54f89e3910 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 12 Feb 2016 14:38:13 +1100 Subject: [PATCH 41/54] New product count for inventory takes permissions into account --- .../filters/inventory_products_filter.js.coffee | 2 +- app/views/admin/variant_overrides/_new_products_alert.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee index 6025e43826..95b8786902 100644 --- a/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee @@ -13,5 +13,5 @@ angular.module("admin.variantOverrides").filter "inventoryProducts", ($filter, I else # Important to only return if true, as other variants for this product might be visible return true if views.new.visible - false + false , true) diff --git a/app/views/admin/variant_overrides/_new_products_alert.html.haml b/app/views/admin/variant_overrides/_new_products_alert.html.haml index 2d060033c5..8892e6484d 100644 --- a/app/views/admin/variant_overrides/_new_products_alert.html.haml +++ b/app/views/admin/variant_overrides/_new_products_alert.html.haml @@ -1,4 +1,4 @@ -%div{ ng: { show: '(newProductCount = (products | newInventoryProducts:hub_id).length) > 0 && !views.new.visible && !alertDismissed' } } +%div{ ng: { show: '(newProductCount = (products | hubPermissions:hubPermissions:hub.id | newInventoryProducts:hub_id).length) > 0 && !views.new.visible && !alertDismissed' } } %hr.divider.sixteen.columns.alpha.omega %alert-row{ message: "#{t('admin.inventory.new_products_alert_message', new_product_count: '{{ newProductCount }}')}", dismissed: "alertDismissed", From 1d83809866a93f91a507991c12b4e60630ab6fc0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sat, 13 Feb 2016 16:14:19 +1100 Subject: [PATCH 42/54] Bugfix: Spree::Variant#not_hidden_for scope was broken Was getting confused by presence of inventory items for other enterprises when none existed for a given variant/enterprise combo --- app/models/spree/variant_decorator.rb | 6 +++-- spec/models/spree/variant_spec.rb | 32 ++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 1dd93c9626..ea04a1a5ac 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -44,9 +44,11 @@ Spree::Variant.class_eval do scope :visible_for, lambda { |enterprise| joins(:inventory_items).where('inventory_items.enterprise_id = (?) AND inventory_items.visible = (?)', enterprise, true) } + scope :not_hidden_for, lambda { |enterprise| - joins('LEFT OUTER JOIN inventory_items ON inventory_items.variant_id = spree_variants.id') - .where('inventory_items.id IS NULL OR (inventory_items.enterprise_id = (?) AND inventory_items.visible != (?))', enterprise, false) + return where("1=0") unless enterprise.present? + joins("LEFT OUTER JOIN (SELECT * from inventory_items WHERE enterprise_id = #{sanitize enterprise.andand.id}) AS o_inventory_items ON o_inventory_items.variant_id = spree_variants.id") + .where("o_inventory_items.id IS NULL OR o_inventory_items.visible = (?)", true) } # Define sope as class method to allow chaining with other scopes filtering id. diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 306381630f..82eed6ba63 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -120,13 +120,35 @@ module Spree let!(:hidden_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: hidden_variant, visible: false ) } let!(:visible_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: visible_variant, visible: true ) } - context "finding variants that are not hidden from an enterprise's inventory" do - let!(:variants) { Spree::Variant.not_hidden_for(enterprise) } + context "when the enterprise given is nil" do + let!(:variants) { Spree::Variant.not_hidden_for(nil) } - it "lists any variants that are not listed as visible=false" do - expect(variants).to include new_variant, visible_variant - expect(variants).to_not include hidden_variant + it "returns an empty list" do + expect(variants).to eq [] + end + end + + context "when an enterprise is given" do + let!(:variants) { Spree::Variant.not_hidden_for(enterprise) } + + it "lists any variants that are not listed as visible=false" do + expect(variants).to include new_variant, visible_variant + expect(variants).to_not include hidden_variant + end + + context "when inventory items exist for other enterprises" do + let(:other_enterprise) { create(:distributor_enterprise) } + + let!(:new_inventory_item) { create(:inventory_item, enterprise: other_enterprise, variant: new_variant, visible: true ) } + let!(:hidden_inventory_item2) { create(:inventory_item, enterprise: other_enterprise, variant: visible_variant, visible: false ) } + let!(:visible_inventory_item2) { create(:inventory_item, enterprise: other_enterprise, variant: hidden_variant, visible: true ) } + + it "lists any variants that are not listed as visible=false only for the relevant enterprise" do + expect(variants).to include new_variant, visible_variant + expect(variants).to_not include hidden_variant + end + end end end From ed40ebace61abde4b14d9f89466d326bcd45547d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 18 Feb 2016 09:53:35 +1100 Subject: [PATCH 43/54] Existing Exchange Variants must be explicitly set to true by form data to remain in an exchange when an order cycle is updated --- .../order_cycle_form_applicator.rb | 9 +++-- .../order_cycle_form_applicator_spec.rb | 35 ++++++++++++++++--- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/lib/open_food_network/order_cycle_form_applicator.rb b/lib/open_food_network/order_cycle_form_applicator.rb index 3e81d6c9f1..6d2e3e3e53 100644 --- a/lib/open_food_network/order_cycle_form_applicator.rb +++ b/lib/open_food_network/order_cycle_form_applicator.rb @@ -140,8 +140,13 @@ module OpenFoodNetwork end def persisted_variants_hash(exchange) - exchange ||= OpenStruct.new(variants: []) - Hash[ exchange.variants.map{ |v| [v.id, true] } ] + return {} unless exchange + + # When we have permission to edit a variant, mark it for removal here, assuming it will be included again if that is what the use wants + # When we don't have permission to edit a variant and it is already in the exchange, keep it in the exchange. + method_name = "editable_variant_ids_for_#{ exchange.incoming? ? 'incoming' : 'outgoing' }_exchange_between" + editable = send(method_name, exchange.sender, exchange.receiver) + Hash[ exchange.variants.map { |v| [v.id, editable.exclude?(v.id)] } ] end def incoming_exchange_variant_ids(attrs) diff --git a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb index 6cbe6693aa..89dcb364b8 100644 --- a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb +++ b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb @@ -160,12 +160,27 @@ module OpenFoodNetwork context "when an exchange is passed in" do let(:v1) { create(:variant) } - let(:exchange) { create(:exchange, variants: [v1]) } + let(:v2) { create(:variant) } + let(:v3) { create(:variant) } + let(:exchange) { create(:exchange, variants: [v1, v2, v3]) } let(:hash) { applicator.send(:persisted_variants_hash, exchange) } - it "returns a hash with variant ids as keys an all values set to true" do - expect(hash.length).to be 1 - expect(hash[v1.id]).to be true + before do + allow(applicator).to receive(:editable_variant_ids_for_outgoing_exchange_between) { [ v1.id, v2.id ] } + end + + it "returns a hash with variant ids as keys" do + expect(hash.length).to be 3 + expect(hash.keys).to include v1.id, v2.id, v3.id + end + + it "editable variant ids are set to false" do + expect(hash[v1.id]).to be false + expect(hash[v2.id]).to be false + end + + it "and non-editable variant ids are set to true" do + expect(hash[v3.id]).to be true end end end @@ -209,7 +224,7 @@ module OpenFoodNetwork before do applicator.stub(:find_outgoing_exchange) { exchange_mock } applicator.stub(:incoming_variant_ids) { [1, 2, 3, 4] } - expect(applicator).to receive(:editable_variant_ids_for_outgoing_exchange_between). + allow(applicator).to receive(:editable_variant_ids_for_outgoing_exchange_between). with(coordinator_mock, enterprise_mock) { [1, 2, 3] } end @@ -234,6 +249,16 @@ module OpenFoodNetwork expect(ids).to eq [1, 3] end + it "removes variants which the user has permission to remove and that are not included in the submitted data" do + allow(exchange_mock).to receive(:incoming?) { false } + allow(exchange_mock).to receive(:variants) { [double(:variant, id: 1), double(:variant, id: 2), double(:variant, id: 3)] } + allow(exchange_mock).to receive(:sender) { coordinator_mock } + allow(exchange_mock).to receive(:receiver) { enterprise_mock } + applicator.stub(:incoming_variant_ids) { [1, 2, 3] } + ids = applicator.send(:outgoing_exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '3' => true}}) + expect(ids).to eq [1, 3] + end + it "removes variants which are not included in incoming exchanges" do applicator.stub(:incoming_variant_ids) { [1, 2] } applicator.stub(:persisted_variants_hash) { {3 => true} } From 4de0a5c2204eedbccd36dda922587a8da73cdf13 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 19 Feb 2016 10:36:50 +1100 Subject: [PATCH 44/54] Renaming 'Variant Overrides' index page to 'Inventory' --- .../shared/_head/replace_spree_title.html.haml.deface | 7 +++++-- .../add_variant_overrides_tab.html.haml.deface | 2 +- .../admin/enterprises/form/_inventory_settings.html.haml | 2 +- app/views/admin/variant_overrides/_header.html.haml | 6 +++++- config/locales/en.yml | 4 ++++ config/routes.rb | 2 ++ spec/features/admin/variant_overrides_spec.rb | 8 ++++---- 7 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/overrides/spree/admin/shared/_head/replace_spree_title.html.haml.deface b/app/overrides/spree/admin/shared/_head/replace_spree_title.html.haml.deface index 4b9bd4142d..906a09bd8c 100644 --- a/app/overrides/spree/admin/shared/_head/replace_spree_title.html.haml.deface +++ b/app/overrides/spree/admin/shared/_head/replace_spree_title.html.haml.deface @@ -1,4 +1,7 @@ / replace_contents "title" -= t(controller.controller_name, :default => controller.controller_name.titleize) -= " - OFN #{t(:administration)}" \ No newline at end of file +- if content_for? :html_title + = yield :html_title +- else + = t(controller.controller_name, :default => controller.controller_name.titleize) += " - OFN #{t(:administration)}" 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 8db108b4f2..f0e507234a 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 :variant_overrides, label: "Overrides", url: main_app.admin_variant_overrides_path, match_path: '/variant_overrides' += tab :variant_overrides, label: "Inventory", url: main_app.admin_inventory_path, match_path: '/inventory' diff --git a/app/views/admin/enterprises/form/_inventory_settings.html.haml b/app/views/admin/enterprises/form/_inventory_settings.html.haml index ded4f6d497..6a6c079409 100644 --- a/app/views/admin/enterprises/form/_inventory_settings.html.haml +++ b/app/views/admin/enterprises/form/_inventory_settings.html.haml @@ -5,7 +5,7 @@ .alpha.eleven.columns You can opt to use your %strong - %a{href: main_app.admin_variant_overrides_path } inventory + %a{href: main_app.admin_inventory_path } inventory to restrict the list of products that are available to add to your shop in the Order Cycle interface. To enable this functionality, select 'Inventory Only' below. .row .alpha.eleven.columns diff --git a/app/views/admin/variant_overrides/_header.html.haml b/app/views/admin/variant_overrides/_header.html.haml index 7b4a38db47..e4a8e98420 100644 --- a/app/views/admin/variant_overrides/_header.html.haml +++ b/app/views/admin/variant_overrides/_header.html.haml @@ -1,4 +1,8 @@ +- content_for :html_title do + = t("admin.inventory.title") + - content_for :page_title do - Override Product Details + %h1.page-title= t("admin.inventory.title") + %a.with-tip{ 'data-powertip' => "#{t("admin.inventory.description")}" }=t('admin.whats_this') = render :partial => 'spree/admin/shared/product_sub_menu' diff --git a/config/locales/en.yml b/config/locales/en.yml index a72afe655d..7603457981 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -72,7 +72,11 @@ en: actions: Actions viewing: "Viewing: %{current_view_name}" + whats_this: What's this? + inventory: + title: Inventory + description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page sku: SKU price: Price on_hand: On Hand diff --git a/config/routes.rb b/config/routes.rb index 64dbc7c7f8..9821a8b4bf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -104,6 +104,8 @@ Openfoodnetwork::Application.routes.draw do get :move_down end + get '/inventory', to: 'variant_overrides#index' + resources :variant_overrides do post :bulk_update, on: :collection post :bulk_reset, on: :collection diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 392b5b48b3..6f10cd8405 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -22,7 +22,7 @@ feature %q{ describe "selecting a hub" do it "displays a list of hub choices" do - visit '/admin/variant_overrides' + visit '/admin/inventory' page.should have_select2 'hub_id', options: ['', hub.name, hub2.name] end @@ -51,7 +51,7 @@ feature %q{ context "when a hub is selected" do before do - visit '/admin/variant_overrides' + visit '/admin/inventory' select2_select hub.name, from: 'hub_id' end @@ -216,7 +216,7 @@ feature %q{ let!(:inventory_item3) { create(:inventory_item, enterprise: hub, variant: variant3) } before do - visit '/admin/variant_overrides' + visit '/admin/inventory' select2_select hub.name, from: 'hub_id' end @@ -305,7 +305,7 @@ feature %q{ context "when a hub is selected" do before do - visit '/admin/variant_overrides' + visit '/admin/inventory' select2_select hub.name, from: 'hub_id' end From b2657ddc72e2133f02bb9b04fd5ebb1feaf8a89a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 19 Feb 2016 11:44:42 +1100 Subject: [PATCH 45/54] Adding data migration to populate inventories of enterprises that are already using variant overrides --- app/models/inventory_item.rb | 2 +- .../20160218235221_populate_inventories.rb | 22 +++++++++++++++++++ db/schema.rb | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20160218235221_populate_inventories.rb diff --git a/app/models/inventory_item.rb b/app/models/inventory_item.rb index 05198b6dec..2648e38341 100644 --- a/app/models/inventory_item.rb +++ b/app/models/inventory_item.rb @@ -1,5 +1,5 @@ class InventoryItem < ActiveRecord::Base - attr_accessible :enterprise_id, :variant_id, :visible + attr_accessible :enterprise, :enterprise_id, :variant, :variant_id, :visible belongs_to :enterprise belongs_to :variant, class_name: "Spree::Variant" diff --git a/db/migrate/20160218235221_populate_inventories.rb b/db/migrate/20160218235221_populate_inventories.rb new file mode 100644 index 0000000000..2b9d1be8b6 --- /dev/null +++ b/db/migrate/20160218235221_populate_inventories.rb @@ -0,0 +1,22 @@ +class PopulateInventories < ActiveRecord::Migration + def up + # If hubs are actively using overrides, populate their inventories with all variants they have permission to override + # Otherwise leave their inventories empty + + hubs_using_overrides = Enterprise.joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.hub_id = enterprises.id") + .where("variant_overrides.id IS NOT NULL").select("DISTINCT enterprises.*") + + hubs_using_overrides.each do |hub| + overridable_producers = OpenFoodNetwork::Permissions.new(hub.owner).variant_override_producers + + variants = Spree::Variant.where(is_master: false, product_id: Spree::Product.not_deleted.where(supplier_id: overridable_producers)) + + variants.each do |variant| + InventoryItem.create(enterprise: hub, variant: variant, visible: true) + end + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index ac02e75ff4..5e62d66530 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 => 20160204031816) do +ActiveRecord::Schema.define(:version => 20160218235221) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false From e00fe824ac637146918afa40c699fa5656c85180 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 24 Feb 2016 17:44:23 +1100 Subject: [PATCH 46/54] Updating text for inventory only product selection setting for enterprises --- .../form/_inventory_settings.html.haml | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/views/admin/enterprises/form/_inventory_settings.html.haml b/app/views/admin/enterprises/form/_inventory_settings.html.haml index 6a6c079409..0df0fdb35f 100644 --- a/app/views/admin/enterprises/form/_inventory_settings.html.haml +++ b/app/views/admin/enterprises/form/_inventory_settings.html.haml @@ -3,17 +3,16 @@ -# their inventory if they so choose .row .alpha.eleven.columns - You can opt to use your - %strong - %a{href: main_app.admin_inventory_path } inventory - to restrict the list of products that are available to add to your shop in the Order Cycle interface. To enable this functionality, select 'Inventory Only' below. + You may opt to manage stock levels and prices in via your + = succeed "." do + %strong + %a{href: main_app.admin_inventory_path } inventory + If you are using the inventory tool, you can select whether new products added by your suppliers need to be added to your inventory before they can be stocked. If you are not using your inventory to manage your products you should select the 'recommended' option below: .row .alpha.eleven.columns - .three.columns.alpha - = f.label "enterprise_preferred_product_selection_from_inventory_only", t('admin.enterprise.select_outgoing_oc_products_from') - .three.columns - = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "1", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } - = label :enterprise, :preferred_product_selection_from_inventory_only, "Inventory Only" - .five.columns.omega + .five.columns.alpha = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "0", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } - = label :enterprise, :preferred_product_selection_from_inventory_only, "All Available Products (Ignore Inventory)" + = label :enterprise, :preferred_product_selection_from_inventory_only, "New products can be put into my shopfront (recommended)" + .six.columns.omega + = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "1", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } + = label :enterprise, :preferred_product_selection_from_inventory_only, "New products must be added to my inventory before they can be put into my shopfront" From 186d1c1f26050d1e685f56344027ab5964ce1fe8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 25 Feb 2016 09:15:20 +1100 Subject: [PATCH 47/54] Revoking ability to create variant overrides via OC permission --- app/models/enterprise.rb | 2 +- lib/open_food_network/permissions.rb | 4 ++-- spec/features/admin/variant_overrides_spec.rb | 16 ++++++++-------- spec/lib/open_food_network/permissions_spec.rb | 12 +++--------- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 9979f06c0d..bc46cbd58f 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -171,7 +171,7 @@ class Enterprise < ActiveRecord::Base if user.has_spree_role?('admin') scoped else - joins(:enterprise_roles).where('enterprise_roles.user_id = ?', user.id).select("DISTINCT enterprises.*") + joins(:enterprise_roles).where('enterprise_roles.user_id = ?', user.id) end } diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 2e6329bd4b..28e5ef274c 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -26,7 +26,7 @@ module OpenFoodNetwork end def variant_override_hubs - managed_and_related_enterprises_granting(:add_to_order_cycle).is_hub + managed_enterprises.is_distributor end def variant_override_producers @@ -38,7 +38,7 @@ module OpenFoodNetwork # override variants # {hub1_id => [producer1_id, producer2_id, ...], ...} def variant_override_enterprises_per_hub - hubs = managed_and_related_enterprises_granting(:add_to_order_cycle).is_distributor + hubs = variant_override_hubs # Permissions granted by create_variant_overrides relationship from producer to hub permissions = Hash[ diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 6f10cd8405..1eceb76021 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' feature %q{ As an Administrator - With products I can add to order cycles + With products I can add to my hub's inventory I want to override the stock level and price of those products Without affecting other hubs that share the same products }, js: true do @@ -11,20 +11,20 @@ feature %q{ let!(:hub) { create(:distributor_enterprise) } let!(:hub2) { create(:distributor_enterprise) } - let!(:hub3) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } - 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: [hub2, producer] } + 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 + let!(:er1) { create(:enterprise_relationship, parent: hub2, child: producer, + permissions_list: [:add_to_order_cycle]) } # This er should not confer ability to create VOs for hub2 + + it "displays a list of hub choices (ie. only those managed by the user)" do visit '/admin/inventory' - page.should have_select2 'hub_id', options: ['', hub.name, hub2.name] + page.should have_select2 'hub_id', options: ['', hub.name] end end @@ -206,7 +206,7 @@ feature %q{ context "with overrides" do let!(:vo) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111, default_stock: 1000, resettable: true) } - let!(:vo_no_auth) { create(:variant_override, variant: variant, hub: hub3, price: 1, count_on_hand: 2) } + let!(:vo_no_auth) { create(:variant_override, variant: variant, hub: hub2, price: 1, count_on_hand: 2) } let!(:product2) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } let!(:variant2) { create(:variant, product: product2, unit_value: 8, price: 1.00, on_hand: 12) } let!(:inventory_item2) { create(:inventory_item, enterprise: hub, variant: variant2) } diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index a91db5f579..5300333111 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -137,22 +137,16 @@ module OpenFoodNetwork 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]) } + permissions_list: [:add_to_order_cycle, :create_variant_overrides]) } 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]} + it "does not allow the user to create variant overrides for the hub" do + permissions.variant_override_enterprises_per_hub.should == {} end end From 51d77d5781ee8458bb69f5cadef92b0b376ee4e6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 25 Feb 2016 09:58:15 +1100 Subject: [PATCH 48/54] Adding migration to explicitly grant VO permission where it is currently implicitly granted via managers/owners In preparation for removing implicitly granted permissions --- ...t_explicit_variant_override_permissions.rb | 24 +++++++++++++++++++ db/schema.rb | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb diff --git a/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb b/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb new file mode 100644 index 0000000000..c8a8795f6f --- /dev/null +++ b/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb @@ -0,0 +1,24 @@ +class GrantExplicitVariantOverridePermissions < ActiveRecord::Migration + def up + hubs = Enterprise.is_distributor + + hubs.each do |hub| + next if hub.owner.admin? + explicitly_granting_producer_ids = hub.relationships_as_child + .with_permission(:create_variant_overrides).map(&:parent_id) + + managed_producer_ids = Enterprise.managed_by(hub.owner).is_primary_producer.pluck(:id) + implicitly_granting_producer_ids = managed_producer_ids - explicitly_granting_producer_ids - [hub.id] + + # create explicit VO permissions for producers currently granting implicit permission + Enterprise.where(id: implicitly_granting_producer_ids).each do |producer| + relationship = producer.relationships_as_parent.find_or_initialize_by_child_id(hub.id) + permission = relationship.permissions.find_or_initialize_by_name(:create_variant_overrides) + relationship.save! unless permission.persisted? + end + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 5e62d66530..9d301086ed 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 => 20160218235221) do +ActiveRecord::Schema.define(:version => 20160224034034) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false From 8e1b4e299c6e0db01a6ac85ccd0c969231fe243d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 25 Feb 2016 15:20:22 +1100 Subject: [PATCH 49/54] Adding column 'permission_revoked_at' to VOs And a migration to flag any VOs which should not be permitted, update previous migration so that new enterprise relationships don't try to revoke variant overrides --- ...t_explicit_variant_override_permissions.rb | 28 +++++++++++-------- ...mission_revoked_at_to_variant_overrides.rb | 21 ++++++++++++++ db/schema.rb | 19 +++++++------ 3 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 db/migrate/20160224230143_add_permission_revoked_at_to_variant_overrides.rb diff --git a/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb b/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb index c8a8795f6f..b56a4a8a93 100644 --- a/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb +++ b/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb @@ -2,20 +2,26 @@ class GrantExplicitVariantOverridePermissions < ActiveRecord::Migration def up hubs = Enterprise.is_distributor - hubs.each do |hub| - next if hub.owner.admin? - explicitly_granting_producer_ids = hub.relationships_as_child - .with_permission(:create_variant_overrides).map(&:parent_id) + begin + EnterpriseRelationship.skip_callback :save, :after, :apply_variant_override_permissions - managed_producer_ids = Enterprise.managed_by(hub.owner).is_primary_producer.pluck(:id) - implicitly_granting_producer_ids = managed_producer_ids - explicitly_granting_producer_ids - [hub.id] + hubs.each do |hub| + next if hub.owner.admin? + explicitly_granting_producer_ids = hub.relationships_as_child + .with_permission(:create_variant_overrides).map(&:parent_id) - # create explicit VO permissions for producers currently granting implicit permission - Enterprise.where(id: implicitly_granting_producer_ids).each do |producer| - relationship = producer.relationships_as_parent.find_or_initialize_by_child_id(hub.id) - permission = relationship.permissions.find_or_initialize_by_name(:create_variant_overrides) - relationship.save! unless permission.persisted? + managed_producer_ids = Enterprise.managed_by(hub.owner).is_primary_producer.pluck(:id) + implicitly_granting_producer_ids = managed_producer_ids - explicitly_granting_producer_ids - [hub.id] + + # create explicit VO permissions for producers currently granting implicit permission + Enterprise.where(id: implicitly_granting_producer_ids).each do |producer| + relationship = producer.relationships_as_parent.find_or_initialize_by_child_id(hub.id) + permission = relationship.permissions.find_or_initialize_by_name(:create_variant_overrides) + relationship.save! unless permission.persisted? + end end + ensure + EnterpriseRelationship.set_callback :save, :after, :apply_variant_override_permissions end end diff --git a/db/migrate/20160224230143_add_permission_revoked_at_to_variant_overrides.rb b/db/migrate/20160224230143_add_permission_revoked_at_to_variant_overrides.rb new file mode 100644 index 0000000000..afa58ebe26 --- /dev/null +++ b/db/migrate/20160224230143_add_permission_revoked_at_to_variant_overrides.rb @@ -0,0 +1,21 @@ +class AddPermissionRevokedAtToVariantOverrides < ActiveRecord::Migration + def up + add_column :variant_overrides, :permission_revoked_at, :datetime, default: nil + + variant_override_hubs = Enterprise.where(id: VariantOverride.all.map(&:hub_id).uniq) + + variant_override_hubs.each do |hub| + permitting_producer_ids = hub.relationships_as_child + .with_permission(:create_variant_overrides).map(&:parent_id) + + variant_overrides_with_revoked_permissions = VariantOverride.for_hubs(hub) + .joins(variant: :product).where("spree_products.supplier_id NOT IN (?)", permitting_producer_ids) + + variant_overrides_with_revoked_permissions.update_all(permission_revoked_at: Time.now) + end + end + + def down + remove_column :variant_overrides, :permission_revoked_at + end +end diff --git a/db/schema.rb b/db/schema.rb index 9d301086ed..b7d42102ed 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 => 20160224034034) do +ActiveRecord::Schema.define(:version => 20160224230143) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -1166,14 +1166,15 @@ ActiveRecord::Schema.define(:version => 20160224034034) do add_index "tags", ["name"], :name => "index_tags_on_name", :unique => true create_table "variant_overrides", :force => true do |t| - t.integer "variant_id", :null => false - t.integer "hub_id", :null => false - t.decimal "price", :precision => 8, :scale => 2 - t.integer "count_on_hand" - t.integer "default_stock" - t.boolean "resettable" - t.string "sku" - t.boolean "on_demand" + t.integer "variant_id", :null => false + t.integer "hub_id", :null => false + t.decimal "price", :precision => 8, :scale => 2 + t.integer "count_on_hand" + t.integer "default_stock" + t.boolean "resettable" + t.string "sku" + t.boolean "on_demand" + t.datetime "permission_revoked_at" end add_index "variant_overrides", ["variant_id", "hub_id"], :name => "index_variant_overrides_on_variant_id_and_hub_id" From 4bf27982f4e5a23825fb19d49bf58f229080e564 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 25 Feb 2016 18:32:10 +1100 Subject: [PATCH 50/54] Applying variant override permissions when they are added or removed Also remove variant overrides with revoked permissions from the default scope --- app/models/enterprise_relationship.rb | 24 +++- app/models/variant_override.rb | 2 + spec/models/enterprise_relationship_spec.rb | 119 ++++++++++++++++++-- spec/models/variant_override_spec.rb | 6 + 4 files changed, 141 insertions(+), 10 deletions(-) diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 8d279b2a20..da0996fdc5 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -6,6 +6,8 @@ class EnterpriseRelationship < ActiveRecord::Base validates_presence_of :parent_id, :child_id validates_uniqueness_of :child_id, scope: :parent_id, message: "^That relationship is already established." + after_save :apply_variant_override_permissions + scope :with_enterprises, joins('LEFT JOIN enterprises AS parent_enterprises ON parent_enterprises.id = enterprise_relationships.parent_id'). joins('LEFT JOIN enterprises AS child_enterprises ON child_enterprises.id = enterprise_relationships.child_id') @@ -61,10 +63,28 @@ class EnterpriseRelationship < ActiveRecord::Base def permissions_list=(perms) - perms.andand.each { |name| permissions.build name: name } + if perms.nil? + permissions.destroy_all + else + permissions.where('name NOT IN (?)', perms).destroy_all + perms.map { |name| permissions.find_or_initialize_by_name name } + end end def has_permission?(name) - permissions.map(&:name).map(&:to_sym).include? name.to_sym + permissions(:reload).map(&:name).map(&:to_sym).include? name.to_sym + end + + private + + def apply_variant_override_permissions + variant_overrides = VariantOverride.unscoped.for_hubs(child) + .joins(variant: :product).where("spree_products.supplier_id IN (?)", parent) + + if has_permission?(:create_variant_overrides) + variant_overrides.update_all(permission_revoked_at: nil) + else + variant_overrides.update_all(permission_revoked_at: Time.now) + end end end diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index 21820ce0db..baede3e62d 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -6,6 +6,8 @@ class VariantOverride < ActiveRecord::Base # Default stock can be nil, indicating stock should not be reset or zero, meaning reset to zero. Need to ensure this can be set by the user. validates :default_stock, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true + default_scope where(permission_revoked_at: nil) + scope :for_hubs, lambda { |hubs| where(hub_id: hubs) } diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index fcdfa8b250..ca21cc7dfc 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -31,16 +31,51 @@ describe EnterpriseRelationship do end describe "creating with a permission list" do - it "creates permissions with a list" do - er = EnterpriseRelationship.create! parent: e1, child: e2, permissions_list: ['one', 'two'] - er.reload - er.permissions.map(&:name).should match_array ['one', 'two'] + context "creating a new list of permissions" do + it "creates a new permission for each item in the list" do + er = EnterpriseRelationship.create! parent: e1, child: e2, permissions_list: ['one', 'two'] + er.reload + er.permissions.map(&:name).should match_array ['one', 'two'] + end + + it "does nothing when the list is nil" do + er = EnterpriseRelationship.create! parent: e1, child: e2, permissions_list: nil + er.reload + er.permissions.should be_empty + end end - it "does nothing when the list is nil" do - er = EnterpriseRelationship.create! parent: e1, child: e2, permissions_list: nil - er.reload - er.permissions.should be_empty + context "updating an existing list of permissions" do + let(:er) { create(:enterprise_relationship, parent: e1, child: e2, permissions_list: ["one", "two", "three"]) } + it "creates a new permission for each item in the list that has no existing permission" do + er.permissions_list = ['four'] + er.save! + er.reload + er.permissions.map(&:name).should include 'four' + end + + it "does not duplicate existing permissions" do + er.permissions_list = ["one", "two", "three"] + er.save! + er.reload + er.permissions.map(&:name).count.should == 3 + er.permissions.map(&:name).should match_array ["one", "two", "three"] + end + + it "removes permissions that are not in the list" do + er.permissions_list = ['one', 'three'] + er.save! + er.reload + er.permissions.map(&:name).should include 'one', 'three' + er.permissions.map(&:name).should_not include 'two' + end + + it "does removes all permissions when the list provided is nil" do + er.permissions_list = nil + er.save! + er.reload + er.permissions.should be_empty + end end end @@ -103,4 +138,72 @@ describe EnterpriseRelationship do EnterpriseRelationship.relatives[e2.id][:producers].should == Set.new([e1.id]) end end + + describe "callbacks" do + context "applying variant override permissions" do + let(:hub) { create(:distributor_enterprise) } + let(:producer) { create(:supplier_enterprise) } + let(:some_other_producer) { create(:supplier_enterprise) } + + context "when variant_override permission is present" do + let!(:er) { create(:enterprise_relationship, child: hub, parent: producer, permissions_list: [:add_to_order_cycles, :create_variant_overrides] )} + let!(:some_other_er) { create(:enterprise_relationship, child: hub, parent: some_other_producer, permissions_list: [:add_to_order_cycles, :create_variant_overrides] )} + let!(:vo1) { create(:variant_override, hub: hub, variant: create(:variant, product: create(:product, supplier: producer))) } + let!(:vo2) { create(:variant_override, hub: hub, variant: create(:variant, product: create(:product, supplier: producer))) } + let!(:vo3) { create(:variant_override, hub: hub, variant: create(:variant, product: create(:product, supplier: some_other_producer))) } + + context "and is then removed" do + before { er.permissions_list = [:add_to_order_cycles]; er.save! } + it "should set permission_revoked_at to the current time for all relevant variant overrides" do + expect(vo1.reload.permission_revoked_at).to_not be_nil + expect(vo2.reload.permission_revoked_at).to_not be_nil + end + + it "should not affect other variant overrides" do + expect(vo3.reload.permission_revoked_at).to be_nil + end + end + + context "and then some other permission is removed" do + before { er.permissions_list = [:create_variant_overrides]; er.save! } + + it "should have no effect on existing variant_overrides" do + expect(vo1.reload.permission_revoked_at).to be_nil + expect(vo2.reload.permission_revoked_at).to be_nil + expect(vo3.reload.permission_revoked_at).to be_nil + end + end + end + + context "when variant_override permission is not present" do + let!(:er) { create(:enterprise_relationship, child: hub, parent: producer, permissions_list: [:add_to_order_cycles] )} + let!(:some_other_er) { create(:enterprise_relationship, child: hub, parent: some_other_producer, permissions_list: [:add_to_order_cycles] )} + let!(:vo1) { create(:variant_override, hub: hub, variant: create(:variant, product: create(:product, supplier: producer)), permission_revoked_at: Time.now) } + let!(:vo2) { create(:variant_override, hub: hub, variant: create(:variant, product: create(:product, supplier: producer)), permission_revoked_at: Time.now) } + let!(:vo3) { create(:variant_override, hub: hub, variant: create(:variant, product: create(:product, supplier: some_other_producer)), permission_revoked_at: Time.now) } + + context "and is then added" do + before { er.permissions_list = [:add_to_order_cycles, :create_variant_overrides]; er.save! } + it "should set permission_revoked_at to nil for all relevant variant overrides" do + expect(vo1.reload.permission_revoked_at).to be_nil + expect(vo2.reload.permission_revoked_at).to be_nil + end + + it "should not affect other variant overrides" do + expect(vo3.reload.permission_revoked_at).to_not be_nil + end + end + + context "and then some other permission is added" do + before { er.permissions_list = [:add_to_order_cycles, :manage_products]; er.save! } + + it "should have no effect on existing variant_overrides" do + expect(vo1.reload.permission_revoked_at).to_not be_nil + expect(vo2.reload.permission_revoked_at).to_not be_nil + expect(vo3.reload.permission_revoked_at).to_not be_nil + end + end + end + end + end end diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb index ed2f2c00f4..9efc53b521 100644 --- a/spec/models/variant_override_spec.rb +++ b/spec/models/variant_override_spec.rb @@ -10,6 +10,12 @@ describe VariantOverride do let(:v) { create(:variant) } let!(:vo1) { create(:variant_override, hub: hub1, variant: v) } let!(:vo2) { create(:variant_override, hub: hub2, variant: v) } + let!(:vo3) { create(:variant_override, hub: hub1, variant: v, permission_revoked_at: Time.now) } + + it "ignores variant_overrides with revoked_permissions by default" do + expect(VariantOverride.all).to_not include vo3 + expect(VariantOverride.unscoped).to include vo3 + end it "finds variant overrides for a set of hubs" do VariantOverride.for_hubs([hub1, hub2]).should match_array [vo1, vo2] From 3f466e86b62c123930050cd05a2b2f57bfdec6b6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 26 Feb 2016 00:17:36 +1100 Subject: [PATCH 51/54] Renaming 'Override Variant Details' permission to 'Add Products To Inventory' Style changes to make enterprise relationships page more useable --- .../services/enterprise_relationships.js.coffee | 2 +- .../enterprise_relationships/_form.html.haml | 4 ++-- .../enterprise_relationships/index.html.haml | 6 ++++++ .../admin/enterprise_relationships_spec.rb | 16 ++++++++-------- .../enterprise_relationships_spec.js.coffee | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index 8c7d798138..0fcd41969c 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -29,4 +29,4 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris when "add_to_order_cycle" then "add to order cycle" when "manage_products" then "manage products" when "edit_profile" then "edit profile" - when "create_variant_overrides" then "override variant details" + when "create_variant_overrides" then "add products to inventory" diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index 433f9f4ca0..b130faeb1a 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -1,11 +1,11 @@ %tr %td - %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} + %select.select2.fullwidth{id: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} %td permits %td - %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} + %select.select2.fullwidth{id: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} %td %label %input{type: "checkbox", ng: {checked: "allPermissionsChecked()", click: "checkAllPermissions()"}} diff --git a/app/views/admin/enterprise_relationships/index.html.haml b/app/views/admin/enterprise_relationships/index.html.haml index 40fbdbc415..163200a0fc 100644 --- a/app/views/admin/enterprise_relationships/index.html.haml +++ b/app/views/admin/enterprise_relationships/index.html.haml @@ -9,6 +9,12 @@ = render 'search_input' %table#enterprise-relationships + %colgroup + %col{ style: "width: 30%" } + %col{ style: "width: 5%" } + %col{ style: "width: 30%" } + %col{ style: "width: 30%" } + %col{ style: "width: 5%" } %tbody = render 'form' = render 'enterprise_relationship' diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index ef0f1e5537..85a0a87ee2 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -37,17 +37,17 @@ feature %q{ e2 = create(:enterprise, name: 'Two') visit admin_enterprise_relationships_path - select 'One', from: 'enterprise_relationship_parent_id' + select2_select 'One', from: 'enterprise_relationship_parent_id' check 'to add to order cycle' check 'to manage products' uncheck 'to manage products' check 'to edit profile' - check 'to override variant details' - select 'Two', from: 'enterprise_relationship_child_id' + check 'to add products to inventory' + select2_select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2, ['to add to order cycle', 'to override variant details', 'to edit profile'] + page.should have_relationship e1, e2, ['to add to order cycle', 'to add products to inventory', 'to edit profile'] er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first er.should be_present er.permissions.map(&:name).should match_array ['add_to_order_cycle', 'edit_profile', 'create_variant_overrides'] @@ -62,8 +62,8 @@ feature %q{ expect do # When I attempt to create a duplicate relationship visit admin_enterprise_relationships_path - select 'One', from: 'enterprise_relationship_parent_id' - select 'Two', from: 'enterprise_relationship_child_id' + select2_select 'One', from: 'enterprise_relationship_parent_id' + select2_select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' # Then I should see an error message @@ -110,8 +110,8 @@ feature %q{ scenario "enterprise user can only add their own enterprises as parent" do visit admin_enterprise_relationships_path - page.should have_select 'enterprise_relationship_parent_id', options: ['', d1.name] - page.should have_select 'enterprise_relationship_child_id', options: ['', d1.name, d2.name, d3.name] + page.should have_select2 'enterprise_relationship_parent_id', options: ['', d1.name] + page.should have_select2 'enterprise_relationship_child_id', options: ['', d1.name, d2.name, d3.name] end 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 0d0a01215d..96ac4a8905 100644 --- a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -15,4 +15,4 @@ describe "enterprise relationships", -> expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "add to order cycle" expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "manage products" expect(EnterpriseRelationships.permission_presentation("edit_profile")).toEqual "edit profile" - expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "override variant details" + expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "add products to inventory" From aff346071f79d7d8acf964ae725395fe2e3c4fa5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 26 Feb 2016 13:49:34 +1100 Subject: [PATCH 52/54] Automatically selecting a hub on inventory page when it is the only option --- .../variant_overrides_controller.js.coffee | 6 +--- .../directives/track_inheritance.js.coffee | 4 +-- .../track_variant_override.js.coffee | 2 +- .../variant_overrides/_controls.html.haml | 4 +-- .../variant_overrides/_filters.html.haml | 12 ++++---- .../_hidden_products.html.haml | 4 +-- .../_loading_flash.html.haml | 2 +- .../variant_overrides/_new_products.html.haml | 6 ++-- .../_new_products_alert.html.haml | 2 +- .../variant_overrides/_no_results.html.haml | 2 +- .../variant_overrides/_products.html.haml | 2 +- .../_products_variants.html.haml | 16 +++++------ .../admin/variant_overrides/index.html.haml | 2 +- spec/features/admin/variant_overrides_spec.rb | 2 +- ...ariant_overrides_controller_spec.js.coffee | 28 +++++++++---------- 15 files changed, 45 insertions(+), 49 deletions(-) diff --git a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee index b754c9b722..26d1e05250 100644 --- a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee @@ -1,6 +1,6 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, Views, SpreeApiAuth, PagedFetcher, StatusMessage, RequestMonitor, hubs, producers, hubPermissions, InventoryItems, VariantOverrides, DirtyVariantOverrides) -> $scope.hubs = Indexer.index hubs - $scope.hub = null + $scope.hub_id = if hubs.length == 1 then hubs[0].id else null $scope.products = [] $scope.producers = producers $scope.producersByID = Indexer.index producers @@ -59,10 +59,6 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", $scope.products = $scope.products.concat products VariantOverrides.ensureDataFor hubs, products - - $scope.selectHub = -> - $scope.hub = $scope.hubs[$scope.hub_id] - $scope.displayDirty = -> if DirtyVariantOverrides.count() > 0 num = if DirtyVariantOverrides.count() == 1 then "one override" else "#{DirtyVariantOverrides.count()} overrides" diff --git a/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee b/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee index 20ac08c035..e0a67eb7d0 100644 --- a/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee @@ -2,11 +2,11 @@ angular.module("admin.variantOverrides").directive "trackInheritance", (VariantO require: "ngModel" link: (scope, element, attrs, ngModel) -> # This is a bit hacky, but it allows us to load the inherit property on the VO, but then not submit it - scope.inherit = angular.equals scope.variantOverrides[scope.hub.id][scope.variant.id], VariantOverrides.newFor scope.hub.id, scope.variant.id + scope.inherit = angular.equals scope.variantOverrides[scope.hub_id][scope.variant.id], VariantOverrides.newFor scope.hub_id, scope.variant.id ngModel.$parsers.push (viewValue) -> if ngModel.$dirty && viewValue - variantOverride = VariantOverrides.inherit(scope.hub.id, scope.variant.id) + variantOverride = VariantOverrides.inherit(scope.hub_id, scope.variant.id) DirtyVariantOverrides.add variantOverride scope.displayDirty() viewValue diff --git a/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee b/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee index 919533967c..184b7af232 100644 --- a/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee @@ -3,7 +3,7 @@ angular.module("admin.variantOverrides").directive "ofnTrackVariantOverride", (D link: (scope, element, attrs, ngModel) -> ngModel.$parsers.push (viewValue) -> if ngModel.$dirty - variantOverride = scope.variantOverrides[scope.hub.id][scope.variant.id] + variantOverride = scope.variantOverrides[scope.hub_id][scope.variant.id] scope.inherit = false DirtyVariantOverrides.add variantOverride scope.displayDirty() diff --git a/app/views/admin/variant_overrides/_controls.html.haml b/app/views/admin/variant_overrides/_controls.html.haml index 3027ad7082..4a25677be3 100644 --- a/app/views/admin/variant_overrides/_controls.html.haml +++ b/app/views/admin/variant_overrides/_controls.html.haml @@ -1,5 +1,5 @@ -%hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'hub && products.length > 0' } } -.controls.sixteen.columns.alpha.omega{ ng: { show: 'hub && products.length > 0' } } +%hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'hub_id && products.length > 0' } } +.controls.sixteen.columns.alpha.omega{ ng: { show: 'hub_id && products.length > 0' } } .eight.columns.alpha = render 'admin/shared/bulk_actions_dropdown' = render 'admin/shared/views_dropdown' diff --git a/app/views/admin/variant_overrides/_filters.html.haml b/app/views/admin/variant_overrides/_filters.html.haml index 90c5e15c4f..7330c26799 100644 --- a/app/views/admin/variant_overrides/_filters.html.haml +++ b/app/views/admin/variant_overrides/_filters.html.haml @@ -1,17 +1,17 @@ .filters.sixteen.columns.alpha .filter.four.columns.alpha - %label{ :for => 'query', ng: {class: '{disabled: !hub.id}'} }=t('admin.quick_search') + %label{ :for => 'query', ng: {class: '{disabled: !hub_id}'} }=t('admin.quick_search') %br - %input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query', disabled: '!hub.id'} } + %input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query', disabled: '!hub_id'} } .two.columns   .filter_select.four.columns %label{ :for => 'hub_id', ng: { bind: "hub_id ? '#{t('admin.shop')}' : '#{t('admin.inventory.select_a_shop')}'" } } %br - %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', ng: { options: 'hub.id as hub.name for (id, hub) in hubs', change: 'selectHub()' } } + %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', ng: { options: 'hub.id as hub.name for (id, hub) in hubs' } } .filter_select.four.columns - %label{ :for => 'producer_filter', ng: {class: '{disabled: !hub.id}'} }=t('admin.producer') + %label{ :for => 'producer_filter', ng: {class: '{disabled: !hub_id}'} }=t('admin.producer') %br - %input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', data: 'producers', blank: "{id: 0, name: 'All'}", ng: { model: 'producerFilter', disabled: '!hub.id' } } + %input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', data: 'producers', blank: "{id: 0, name: 'All'}", ng: { model: 'producerFilter', disabled: '!hub_id' } } -# .filter_select{ :class => "three columns" } -# %label{ :for => 'distributor_filter' }Hub -# %br @@ -23,4 +23,4 @@ .filter_clear.two.columns.omega %label{ :for => 'clear_all_filters' } %br - %input.red.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "#{t('admin.clear_all')}", ng: { click: "resetSelectFilters()", disabled: '!hub.id'} } + %input.red.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "#{t('admin.clear_all')}", ng: { click: "resetSelectFilters()", disabled: '!hub_id'} } diff --git a/app/views/admin/variant_overrides/_hidden_products.html.haml b/app/views/admin/variant_overrides/_hidden_products.html.haml index 064b99d19a..902e24a232 100644 --- a/app/views/admin/variant_overrides/_hidden_products.html.haml +++ b/app/views/admin/variant_overrides/_hidden_products.html.haml @@ -11,12 +11,12 @@ %th.variant=t('(admin.variant') %th.add=t('admin.inventory.add') %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } - %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub.id:views' } } + %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub_id:views' } } %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } %td.product{ bo: { bind: 'product.name'} } %td.variant %span{ bo: { bind: 'variant.display_name || ""'} } .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } %td.add - %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub.id,variant.id,true)" } } + %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub_id,variant.id,true)" } } = t('admin.inventory.add') diff --git a/app/views/admin/variant_overrides/_loading_flash.html.haml b/app/views/admin/variant_overrides/_loading_flash.html.haml index 24cc7c041d..c543a5517c 100644 --- a/app/views/admin/variant_overrides/_loading_flash.html.haml +++ b/app/views/admin/variant_overrides/_loading_flash.html.haml @@ -1,3 +1,3 @@ -%div.sixteen.columns.alpha.omega#loading{ ng: { cloak: true, if: 'hub && products.length == 0 && RequestMonitor.loading' } } +%div.sixteen.columns.alpha.omega#loading{ ng: { cloak: true, if: 'hub_id && products.length == 0 && RequestMonitor.loading' } } %img.spinner{ src: "/assets/spinning-circles.svg" } %h1 LOADING INVENTORY diff --git a/app/views/admin/variant_overrides/_new_products.html.haml b/app/views/admin/variant_overrides/_new_products.html.haml index affb52b066..86ca180b8f 100644 --- a/app/views/admin/variant_overrides/_new_products.html.haml +++ b/app/views/admin/variant_overrides/_new_products.html.haml @@ -12,15 +12,15 @@ %th.add=t('admin.inventory.add') %th.hide=t('admin.inventory.hide') %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } - %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub.id:views' } } + %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub_id:views' } } %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } %td.product{ bo: { bind: 'product.name'} } %td.variant %span{ bo: { bind: 'variant.display_name || ""'} } .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } %td.add - %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub.id,variant.id,true)" } } + %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub_id,variant.id,true)" } } = t('admin.inventory.add') %td.hide - %button.fullwidth.hide.icon-remove{ ng: { click: "setVisibility(hub.id,variant.id,false)" } } + %button.fullwidth.hide.icon-remove{ ng: { click: "setVisibility(hub_id,variant.id,false)" } } = t('admin.inventory.hide') diff --git a/app/views/admin/variant_overrides/_new_products_alert.html.haml b/app/views/admin/variant_overrides/_new_products_alert.html.haml index 8892e6484d..29ec4c9623 100644 --- a/app/views/admin/variant_overrides/_new_products_alert.html.haml +++ b/app/views/admin/variant_overrides/_new_products_alert.html.haml @@ -1,4 +1,4 @@ -%div{ ng: { show: '(newProductCount = (products | hubPermissions:hubPermissions:hub.id | newInventoryProducts:hub_id).length) > 0 && !views.new.visible && !alertDismissed' } } +%div{ ng: { show: '(newProductCount = (products | hubPermissions:hubPermissions:hub_id | newInventoryProducts:hub_id).length) > 0 && !views.new.visible && !alertDismissed' } } %hr.divider.sixteen.columns.alpha.omega %alert-row{ message: "#{t('admin.inventory.new_products_alert_message', new_product_count: '{{ newProductCount }}')}", dismissed: "alertDismissed", diff --git a/app/views/admin/variant_overrides/_no_results.html.haml b/app/views/admin/variant_overrides/_no_results.html.haml index 628be090ad..cdec6ab8c5 100644 --- a/app/views/admin/variant_overrides/_no_results.html.haml +++ b/app/views/admin/variant_overrides/_no_results.html.haml @@ -1,4 +1,4 @@ -%div.text-big.no-results{ ng: { show: 'hub && products.length > 0 && filteredProducts.length == 0' } } +%div.text-big.no-results{ ng: { show: 'hub_id && products.length > 0 && filteredProducts.length == 0' } } %span{ ng: { show: 'views.inventory.visible && !filtersApplied()' } }=t('admin.inventory.currently_empty') %span{ ng: { show: 'views.inventory.visible && filtersApplied()' } }=t('admin.inventory.no_matching_products') %span{ ng: { show: 'views.hidden.visible && !filtersApplied()' } }=t('admin.inventory.no_hidden_products') diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index d453dffbd7..c17e4c189a 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -22,6 +22,6 @@ %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } }=t('admin.inventory.enable_reset') %th.inheritance{ ng: { show: 'columns.inheritance.visible' } }=t('admin.inventory.inherit') %th.visibility{ ng: { show: 'columns.visibility.visible' } }=t('admin.inventory.hide') - %tbody{bindonce: true, ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub.id | inventoryProducts:hub.id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } + %tbody{bindonce: true, ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } = render 'admin/variant_overrides/products_product' = render 'admin/variant_overrides/products_variants' diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index e9a9e12a15..c26be92697 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -1,22 +1,22 @@ -%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants | inventoryVariants:hub.id:views'}} +%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants | inventoryVariants:hub_id:views'}} %td.producer{ ng: { show: 'columns.producer.visible' } } %td.product{ ng: { show: 'columns.product.visible' } } %span{ bo: { bind: 'variant.display_name || ""'} } .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } %td.sku{ ng: { show: 'columns.sku.visible' } } - %input{name: 'variant-overrides-{{ variant.id }}-sku', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].sku'}, placeholder: '{{ variant.sku }}', 'ofn-track-variant-override' => 'sku'} + %input{name: 'variant-overrides-{{ variant.id }}-sku', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].sku'}, placeholder: '{{ variant.sku }}', 'ofn-track-variant-override' => 'sku'} %td.price{ ng: { show: 'columns.price.visible' } } - %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'} + %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.on_hand{ ng: { show: 'columns.on_hand.visible' } } - %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' => 'count_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' => 'count_on_hand'} %td.on_demand{ ng: { show: 'columns.on_demand.visible' } } - %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-on_demand', ng: { model: 'variantOverrides[hub.id][variant.id].on_demand' }, 'ofn-track-variant-override' => 'on_demand' } + %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-on_demand', ng: { model: 'variantOverrides[hub_id][variant.id].on_demand' }, 'ofn-track-variant-override' => 'on_demand' } %td.reset{ ng: { show: 'columns.reset.visible' } } - %input{name: 'variant-overrides-{{ variant.id }}-resettable', type: 'checkbox', ng: {model: 'variantOverrides[hub.id][variant.id].resettable'}, placeholder: '{{ variant.resettable }}', 'ofn-track-variant-override' => 'resettable'} + %input{name: 'variant-overrides-{{ variant.id }}-resettable', type: 'checkbox', ng: {model: 'variantOverrides[hub_id][variant.id].resettable'}, placeholder: '{{ variant.resettable }}', 'ofn-track-variant-override' => 'resettable'} %td.reset{ ng: { show: 'columns.reset.visible' } } - %input{name: 'variant-overrides-{{ variant.id }}-default_stock', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].default_stock'}, placeholder: '{{ variant.default_stock ? variant.default_stock : "Default stock"}}', 'ofn-track-variant-override' => 'default_stock'} + %input{name: 'variant-overrides-{{ variant.id }}-default_stock', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].default_stock'}, placeholder: '{{ variant.default_stock ? variant.default_stock : "Default stock"}}', 'ofn-track-variant-override' => 'default_stock'} %td.inheritance{ ng: { show: 'columns.inheritance.visible' } } %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-inherit', ng: { model: 'inherit' }, 'track-inheritance' => true } %td.visibility{ ng: { show: 'columns.visibility.visible' } } - %button.icon-remove.hide.fullwidth{ :type => 'button', ng: { click: "setVisibility(hub.id,variant.id,false)" } } + %button.icon-remove.hide.fullwidth{ :type => 'button', ng: { click: "setVisibility(hub_id,variant.id,false)" } } = t('admin.inventory.hide') diff --git a/app/views/admin/variant_overrides/index.html.haml b/app/views/admin/variant_overrides/index.html.haml index d939465bac..16beaad7b9 100644 --- a/app/views/admin/variant_overrides/index.html.haml +++ b/app/views/admin/variant_overrides/index.html.haml @@ -7,7 +7,7 @@ = render 'admin/variant_overrides/loading_flash' = render 'admin/variant_overrides/controls' = render 'admin/variant_overrides/no_results' - %div{ ng: { cloak: true, show: 'hub && filteredProducts.length > 0' } } + %div{ ng: { cloak: true, show: 'hub_id && filteredProducts.length > 0' } } = render 'admin/variant_overrides/new_products' = render 'admin/variant_overrides/hidden_products' = render 'admin/variant_overrides/products' diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 1eceb76021..46445c1c63 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -24,7 +24,7 @@ feature %q{ it "displays a list of hub choices (ie. only those managed by the user)" do visit '/admin/inventory' - page.should have_select2 'hub_id', options: ['', hub.name] + page.should have_select2 'hub_id', options: [hub.name] # Selects the hub automatically when only one is available end end 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 75e6102863..de3a3d5d55 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 @@ -28,9 +28,20 @@ describe "VariantOverridesCtrl", -> StatusMessage = _StatusMessage_ ctrl = $controller 'AdminVariantOverridesCtrl', { $scope: scope, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions, VariantOverrides: VariantOverrides, DirtyVariantOverrides: DirtyVariantOverrides, StatusMessage: StatusMessage} - it "initialises the hub list and the chosen hub", -> - expect(scope.hubs).toEqual { 1: {id: 1, name: 'Hub'} } - expect(scope.hub).toBeNull() + describe "when only one hub is available", -> + it "initialises the hub list and the selects the only hub in the list", -> + expect(scope.hubs).toEqual { 1: {id: 1, name: 'Hub'} } + expect(scope.hub_id).toEqual 1 + + describe "when more than one hub is available", -> + beforeEach -> + inject ($controller) -> + hubs = [{id: 1, name: 'Hub1'}, {id: 12, name: 'Hub2'}] + $controller 'AdminVariantOverridesCtrl', { $scope: scope, hubs: hubs, producers: [], products: [], hubPermissions: []} + + it "initialises the hub list and the selects the only hub in the list", -> + expect(scope.hubs).toEqual { 1: {id: 1, name: 'Hub1'}, 12: {id: 12, name: 'Hub2'} } + expect(scope.hub_id).toBeNull() it "initialises select filters", -> expect(scope.producerFilter).toEqual 0 @@ -45,17 +56,6 @@ describe "VariantOverridesCtrl", -> expect(scope.products).toEqual ['a', 'b', 'c', 'd'] expect(VariantOverrides.ensureDataFor).toHaveBeenCalled() - 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 - describe "updating", -> describe "error messages", -> it "returns an unauthorised message upon 401", -> From 465649475d670aeb034a72dfbd6f35658a4f333e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 2 Mar 2016 08:51:54 +1100 Subject: [PATCH 53/54] Revoke ability to overide variants based on shared management/ownership (ie. only explicit permissions) --- lib/open_food_network/permissions.rb | 11 ++++------ spec/features/admin/variant_overrides_spec.rb | 21 +++++++++++++------ .../lib/open_food_network/permissions_spec.rb | 13 ++++++++---- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 28e5ef274c..795bd3cf68 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -49,13 +49,10 @@ module OpenFoodNetwork 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? - hubs.each do |hub| - permissions[hub.id] = ((permissions[hub.id] || []) + managed_producer_ids).uniq - end + # Allow a producer hub to override it's own products without explicit permission + hubs.is_primary_producer.each do |hub| + permissions[hub.id] ||= [] + permissions[hub.id] |= [hub.id] end permissions diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 46445c1c63..fe74d9eb2d 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -12,13 +12,20 @@ feature %q{ let!(:hub) { create(:distributor_enterprise) } let!(:hub2) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } + let!(:producer_managed) { create(:supplier_enterprise) } + let!(:producer_related) { create(:supplier_enterprise) } + let!(:producer_unrelated) { create(:supplier_enterprise) } + let!(:er1) { create(:enterprise_relationship, parent: producer, child: hub, + permissions_list: [:create_variant_overrides]) } + let!(:er2) { create(:enterprise_relationship, parent: producer_related, child: hub, + permissions_list: [:create_variant_overrides]) } context "as an enterprise user" do - let(:user) { create_enterprise_user enterprises: [hub, producer] } + let(:user) { create_enterprise_user enterprises: [hub, producer_managed] } before { quick_login_as user } describe "selecting a hub" do - let!(:er1) { create(:enterprise_relationship, parent: hub2, child: producer, + let!(:er1) { create(:enterprise_relationship, parent: hub2, child: producer_managed, permissions_list: [:add_to_order_cycle]) } # This er should not confer ability to create VOs for hub2 it "displays a list of hub choices (ie. only those managed by the user)" do @@ -33,14 +40,14 @@ feature %q{ let!(:variant) { create(:variant, product: product, unit_value: 1, price: 1.23, on_hand: 12) } let!(:inventory_item) { create(:inventory_item, enterprise: hub, variant: variant ) } - let!(:producer_related) { create(:supplier_enterprise) } + let!(:product_managed) { create(:simple_product, supplier: producer_managed, variant_unit: 'weight', variant_unit_scale: 1) } + let!(:variant_managed) { create(:variant, product: product_managed, unit_value: 3, price: 3.65, on_hand: 2) } + let!(:inventory_item_managed) { create(:inventory_item, enterprise: hub, variant: variant_managed ) } + 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!(:inventory_item_related) { create(:inventory_item, enterprise: hub, variant: variant_related ) } - 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) } @@ -67,6 +74,8 @@ feature %q{ page.should have_input "variant-overrides-#{variant_related.id}-count_on_hand", placeholder: '23' # filters the products to those the hub can override + page.should_not have_content producer_managed.name + page.should_not have_content product_managed.name page.should_not have_content producer_unrelated.name page.should_not have_content product_unrelated.name diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index 5300333111..a26db2aa98 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -119,7 +119,7 @@ module OpenFoodNetwork {hub.id => [producer.id]} end - it "returns only permissions relating to managed enterprises" do + it "returns only permissions relating to managed hubs" do create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:create_variant_overrides]) @@ -150,12 +150,17 @@ module OpenFoodNetwork end end - it "also returns managed producers" do + it "does not return managed producers (ie. only uses explicitly granted VO permissions)" do producer2 = create(:supplier_enterprise) permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub, producer2]) } - permissions.variant_override_enterprises_per_hub.should == - {hub.id => [producer.id, producer2.id]} + expect(permissions.variant_override_enterprises_per_hub[hub.id]).to_not include producer2.id + end + + it "returns itself if self is also a primary producer (even when no explicit permission exists)" do + hub.update_attribute(:is_primary_producer, true) + + expect(permissions.variant_override_enterprises_per_hub[hub.id]).to include hub.id end end From 0d65838e5daf342c2961a96a6832113ed98f8261 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 2 Mar 2016 17:09:39 +1100 Subject: [PATCH 54/54] Adding a second migration to auto-add variants to inventories (I stuffed up the first one, oops) --- .../20160302044850_repopulate_inventories.rb | 30 +++++++++++++++++++ db/schema.rb | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160302044850_repopulate_inventories.rb diff --git a/db/migrate/20160302044850_repopulate_inventories.rb b/db/migrate/20160302044850_repopulate_inventories.rb new file mode 100644 index 0000000000..e38628439a --- /dev/null +++ b/db/migrate/20160302044850_repopulate_inventories.rb @@ -0,0 +1,30 @@ +class RepopulateInventories < ActiveRecord::Migration + # Previous version of this migration (20160218235221) relied on Permissions#variant_override_producers + # which was then changed, meaning that an incomplete set of variants were added to inventories of most hubs + # Re-running this now will ensure that all permitted variants (including those allowed by 20160224034034) are + # added to the relevant inventories + + def up + # If hubs are actively using overrides, populate their inventories with all variants they have permission to override + # Otherwise leave their inventories empty + + hubs_using_overrides = Enterprise.joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.hub_id = enterprises.id") + .where("variant_overrides.id IS NOT NULL").select("DISTINCT enterprises.*") + + hubs_using_overrides.each do |hub| + overridable_producer_ids = hub.relationships_as_child.with_permission(:create_variant_overrides).map(&:parent_id) | [hub.id] + + variants = Spree::Variant.where(is_master: false, product_id: Spree::Product.not_deleted.where(supplier_id: overridable_producer_ids)) + + variants_to_add = variants.joins("LEFT OUTER JOIN (SELECT * from inventory_items WHERE enterprise_id = #{hub.id}) AS o_inventory_items ON o_inventory_items.variant_id = spree_variants.id") + .where('o_inventory_items.id IS NULL') + + variants_to_add.each do |variant| + inventory_item = InventoryItem.create(enterprise: hub, variant: variant, visible: true) + end + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index b7d42102ed..5696e5168a 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 => 20160224230143) do +ActiveRecord::Schema.define(:version => 20160302044850) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false