diff --git a/.travis.yml b/.travis.yml index 0ba99dc9e1..56d94e5952 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ before_script: script: - 'if [ "$KARMA" = "true" ]; then bundle exec rake karma:run; else echo "Skipping karma run"; fi' #- "KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec" - - "bundle exec rake knapsack:rspec" + - "bundle exec rake 'knapsack:rspec[--tag ~performance]'" after_success: - > diff --git a/Gemfile b/Gemfile index 300b17ffa3..d581c8d10b 100644 --- a/Gemfile +++ b/Gemfile @@ -5,16 +5,18 @@ gem 'rails', '3.2.21' gem 'rails-i18n', '~> 3.0.0' gem 'i18n', '~> 0.6.11' -gem 'nokogiri' +# Patched version. See http://rubysec.com/advisories/CVE-2015-5312/. +gem 'nokogiri', '>= 1.6.7.1' 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' -# Waiting on merge of PR #117 -# https://github.com/spree-contrib/better_spree_paypal_express/pull/117 -gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "1-3-stable" +# Our branch contains two changes +# - Pass customer email and phone number to PayPal (merged to upstream master) +# - Change type of password from string to password to hide it in the form +gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "hide-password" #gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable" gem 'delayed_job_active_record' @@ -55,6 +57,7 @@ gem 'figaro' gem 'blockenspiel' gem 'acts-as-taggable-on', '~> 3.4' gem 'paper_trail', '~> 3.0.8' +gem 'diffy' gem 'wicked_pdf' gem 'wkhtmltopdf-binary' diff --git a/Gemfile.lock b/Gemfile.lock index 6dd9888f8c..0e31157d96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,8 +14,8 @@ GIT GIT remote: git://github.com/openfoodfoundation/better_spree_paypal_express.git - revision: cdd61161ccd27cd8d183f9321422c7be113796b8 - branch: 1-3-stable + revision: 840d973cd5bd3250b17674a624dad494aeb09eb3 + branch: hide-password specs: spree_paypal_express (2.0.3) paypal-sdk-merchant (= 1.106.1) @@ -248,6 +248,7 @@ GEM devise-encryptable (0.1.2) devise (>= 2.1.0) diff-lcs (1.2.4) + diffy (3.1.0) em-websocket (0.5.0) eventmachine (>= 0.12.9) http_parser.rb (~> 0.5.3) @@ -449,7 +450,7 @@ GEM treetop (~> 1.4.8) method_source (0.8.2) mime-types (1.25.1) - mini_portile (0.6.2) + mini_portile2 (2.0.0) momentjs-rails (2.5.1) railties (>= 3.1) money (5.1.1) @@ -457,8 +458,8 @@ GEM multi_json (1.11.2) multi_xml (0.5.5) newrelic_rpm (3.12.0.288) - nokogiri (1.6.6.4) - mini_portile (~> 0.6.0) + nokogiri (1.6.7.2) + mini_portile2 (~> 2.0.0.rc2) oj (2.1.2) orm_adapter (0.5.0) paper_trail (3.0.8) @@ -668,6 +669,7 @@ DEPENDENCIES debugger-linecache deface! delayed_job_active_record + diffy factory_girl_rails figaro foreigner @@ -691,7 +693,7 @@ DEPENDENCIES letter_opener momentjs-rails newrelic_rpm - nokogiri + nokogiri (>= 1.6.7.1) oj paper_trail (~> 3.0.8) paperclip @@ -730,3 +732,6 @@ DEPENDENCIES whenever wicked_pdf wkhtmltopdf-binary + +BUNDLED WITH + 1.10.6 diff --git a/app/assets/images/ofn-logo-footer.png b/app/assets/images/ofn-logo-footer.png new file mode 100644 index 0000000000..f612d5aa87 Binary files /dev/null and b/app/assets/images/ofn-logo-footer.png differ diff --git a/app/assets/images/ofn-logo-mobile.svg b/app/assets/images/ofn-logo-mobile.svg new file mode 100644 index 0000000000..7c48b00b1b --- /dev/null +++ b/app/assets/images/ofn-logo-mobile.svg @@ -0,0 +1,80 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/app/assets/images/ofn-logo.png b/app/assets/images/ofn-logo.png new file mode 100644 index 0000000000..f53680c342 Binary files /dev/null and b/app/assets/images/ofn-logo.png differ diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 344c5e8a5d..9f99dc1dcd 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -38,6 +38,7 @@ //= require ./products/products //= require ./shipping_methods/shipping_methods //= require ./side_menu/side_menu +//= require ./tag_rules/tag_rules //= require ./taxons/taxons //= require ./utils/utils //= require ./users/users @@ -45,5 +46,8 @@ //= require textAngular.min.js //= require textAngular-sanitize.min.js //= require ../shared/bindonce.min.js +//= require darkswarm/i18n.js +//= require darkswarm/i18n.translate.js + //= require_tree . diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index 7d0572635d..b747d3699e 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -12,25 +12,25 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ $scope.startDate = formatDate start $scope.endDate = formatDate end $scope.quickSearch = "" - $scope.bulkActions = [ { name: "Delete Selected", callback: $scope.deleteLineItems } ] + $scope.bulkActions = [ { name: t("bom_actions_delete"), callback: $scope.deleteLineItems } ] $scope.selectedBulkAction = $scope.bulkActions[0] $scope.selectedUnitsProduct = {}; $scope.selectedUnitsVariant = {}; $scope.sharedResource = false $scope.columns = Columns.setColumns - order_no: { name: "Order No.", visible: false } - full_name: { name: "Name", visible: true } - email: { name: "Email", visible: false } - phone: { name: "Phone", visible: false } - order_date: { name: "Order Date", visible: true } - producer: { name: "Producer", visible: true } - order_cycle: { name: "Order Cycle", visible: false } - hub: { name: "Hub", visible: false } - variant: { name: "Variant", visible: true } - quantity: { name: "Quantity", visible: true } - max: { name: "Max", visible: true } - final_weight_volume: { name: "Weight/Volume", visible: false } - price: { name: "Price", visible: false } + order_no: { name: t("bom_no"), visible: false } + full_name: { name: t("name"), visible: true } + email: { name: t("email"), visible: false } + phone: { name: t("phone"), visible: false } + order_date: { name: t("bom_date"), visible: true } + producer: { name: t("producer"), visible: true } + order_cycle: { name: t("bom_cycle"), visible: false } + hub: { name: t("bom_hub"), visible: false } + variant: { name: t("bom_variant"), visible: true } + quantity: { name: t("bom_quantity"), visible: true } + max: { name: t("bom_max"), visible: true } + final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false } + price: { name: t("price"), visible: false } $scope.initialise = -> $scope.initialiseVariables() authorise_api_reponse = "" diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 6c85a4fd54..3d565f703a 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -4,32 +4,32 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.StatusMessage = StatusMessage $scope.columns = Columns.setColumns - producer: {name: "Producer", visible: true} - sku: {name: "SKU", visible: false} - name: {name: "Name", visible: true} - unit: {name: "Unit", visible: true} - price: {name: "Price", visible: true} - on_hand: {name: "On Hand", visible: true} - on_demand: {name: "On Demand", visible: false} - category: {name: "Category", visible: false} - tax_category: {name: "Tax Category", visible: false} - inherits_properties: {name: "Inherits Properties?", visible: false} - available_on: {name: "Available On", visible: false} + producer: {name: t("products_producer"), visible: true} + sku: {name: t("products_sku"), visible: false} + name: {name: t("products_name"), visible: true} + unit: {name: t("products_unit"), visible: true} + price: {name: t("products_price"), visible: true} + on_hand: {name: t("products_on_hand"), visible: true} + on_demand: {name: t("products_on_demand"), visible: false} + category: {name: t("products_category"), visible: false} + tax_category: {name: t("products_tax_category"), visible: false} + inherits_properties: {name: t("products_inherits_properties"), visible: false} + available_on: {name: t("products_available_on"), visible: false} $scope.variant_unit_options = VariantUnitManager.variantUnitOptions() $scope.filterableColumns = [ - { name: "Producer", db_column: "producer_name" }, - { name: "Name", db_column: "name" } + { name: t("label_producers"), db_column: "producer_name" }, + { name: t("name"), db_column: "name" } ] $scope.filterTypes = [ - { name: "Equals", predicate: "eq" }, - { name: "Contains", predicate: "cont" } + { name: t("equals"), predicate: "eq" }, + { name: t("contains"), predicate: "cont" } ] $scope.optionTabs = - filters: { title: "Filter Products", visible: false } + filters: { title: t("filter_products"), visible: false } $scope.producers = producers @@ -105,7 +105,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.categoryFilter = "0" $scope.editWarn = (product, variant) -> - if (DirtyProducts.count() > 0 and confirm("Unsaved changes will be lost. Continue anyway?")) or (DirtyProducts.count() == 0) + if (DirtyProducts.count() > 0 and confirm(t("unsaved_changes_confirmation"))) or (DirtyProducts.count() == 0) window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit" @@ -150,14 +150,14 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout if !$scope.variantSaved(variant) $scope.removeVariant(product, variant) else - if confirm("Are you sure?") + if confirm(t("are_you_sure")) $http( method: "DELETE" url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete" ).success (data) -> $scope.removeVariant(product, variant) else - alert("The last variant cannot be deleted!") + alert(t("delete_product_variant")) $scope.removeVariant = (product, variant) -> product.variants.splice product.variants.indexOf(variant), 1 @@ -194,7 +194,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout if productsToSubmit.length > 0 $scope.updateProducts productsToSubmit # Don't submit an empty list else - StatusMessage.display 'alert', 'No changes to save.' + StatusMessage.display 'alert', t("products_change") $scope.updateProducts = (productsToSubmit) -> @@ -212,10 +212,10 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout ).error (data, status) -> if status == 400 && data.errors? && data.errors.length > 0 errors = error + "\n" for error in data.errors - alert "Saving failed with the following error(s):\n" + errors - $scope.displayFailure "Save failed due to invalid data" + alert t("products_update_error") + "\n" + errors + $scope.displayFailure t("products_update_error") else - $scope.displayFailure "Server returned with error status: " + status + $scope.displayFailure t("products_update_error_data") + status $scope.packProduct = (product) -> @@ -253,23 +253,23 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.displayUpdating = -> - StatusMessage.display 'progress', 'Saving...' + StatusMessage.display 'progress', t("saving") $scope.displaySuccess = -> - StatusMessage.display 'success', 'Changes saved.' + StatusMessage.display 'success',t("products_changes_saved") $scope.displayFailure = (failMessage) -> - StatusMessage.display 'failure', "Saving failed. #{failMessage}" + StatusMessage.display 'failure', t("products_update_error_msg") + "#{failMessage}" $scope.displayDirtyProducts = -> - if DirtyProducts.count() > 0 - message = if DirtyProducts.count() == 1 then "one product" else DirtyProducts.count() + " products" - StatusMessage.display 'notice', "Changes to #{message} remain unsaved." - else - StatusMessage.clear() + count = DirtyProducts.count() + switch count + when 0 then StatusMessage.clear() + when 1 then StatusMessage.display 'notice', t("one_product_unsaved") + else StatusMessage.display 'notice', t("products_unsaved", n: count) filterSubmitProducts = (productsToFilter) -> diff --git a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee index 47083d443a..153acb2443 100644 --- a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee @@ -7,7 +7,7 @@ angular.module("ofn.admin").controller "AdminEnterpriseRelationshipsCtrl", ($sco $scope.EnterpriseRelationships.create($scope.parent_id, $scope.child_id, $scope.permissions) $scope.delete = (enterprise_relationship) -> - if confirm("Are you sure?") + if confirm(t("are_you_sure")) $scope.EnterpriseRelationships.delete enterprise_relationship $scope.toggleKeyword = (string, key) -> diff --git a/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee index 026913a263..71d0097c6b 100644 --- a/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee @@ -7,5 +7,5 @@ angular.module("ofn.admin").controller "AdminEnterpriseRolesCtrl", ($scope, Ente $scope.EnterpriseRoles.create($scope.user_id, $scope.enterprise_id) $scope.delete = (enterprise_role) -> - if confirm("Are you sure?") + if confirm(t('are_you_sure')) $scope.EnterpriseRoles.delete enterprise_role diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index c475f1e4df..d2e6d58562 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -1,5 +1,5 @@ -angular.module("admin.customers").controller "customersCtrl", ($scope, Customers, Columns, pendingChanges, shops) -> - $scope.shop = null +angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, Columns, pendingChanges, shops) -> + $scope.shop = {} $scope.shops = shops $scope.submitAll = pendingChanges.submitAll @@ -8,10 +8,26 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, Customers code: { name: "Code", visible: true } tags: { name: "Tags", visible: true } - $scope.$watch "shop", -> - if $scope.shop? - Customers.loaded = false - $scope.customers = Customers.index(enterprise_id: $scope.shop.id) + $scope.$watch "shop.id", -> + if $scope.shop.id? + $scope.customers = index {enterprise_id: $scope.shop.id} - $scope.loaded = -> - Customers.loaded + $scope.add = (email) -> + params = + enterprise_id: $scope.shop.id + email: email + CustomerResource.create params, (customer) => + if customer.id + $scope.customers.push customer + $scope.quickSearch = customer.email + + $scope.deleteCustomer = (customer) -> + params = id: customer.id + CustomerResource.destroy params, -> + i = $scope.customers.indexOf customer + $scope.customers.splice i, 1 unless i < 0 + + index = (params) -> + $scope.loaded = false + CustomerResource.index params, => + $scope.loaded = true diff --git a/app/assets/javascripts/admin/customers/customers.js.coffee b/app/assets/javascripts/admin/customers/customers.js.coffee index 3733fe2eea..1e8ae9b988 100644 --- a/app/assets/javascripts/admin/customers/customers.js.coffee +++ b/app/assets/javascripts/admin/customers/customers.js.coffee @@ -1 +1 @@ -angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.dropdown']) \ No newline at end of file +angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.utils', 'admin.dropdown']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee deleted file mode 100644 index e15ec10342..0000000000 --- a/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -angular.module("admin.customers").directive "tagsWithTranslation", -> - restrict: "E" - template: "" - scope: - object: "=" - link: (scope, element, attrs) -> - scope.$watchCollection "object.tags", -> - scope.object.tag_list = (tag.text for tag in scope.object.tags).join(",") diff --git a/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee b/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee index 523e0c1495..5b6c1ab205 100644 --- a/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee +++ b/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee @@ -1,8 +1,17 @@ angular.module("admin.customers").factory 'CustomerResource', ($resource) -> - $resource('/admin/customers.json', {}, { + $resource('/admin/customers/:id.json', {}, { 'index': method: 'GET' isArray: true params: enterprise_id: '@enterprise_id' + 'create': + method: 'POST' + params: + enterprise_id: '@enterprise_id' + email: '@email' + 'destroy': + method: 'DELETE' + params: + id: '@id' }) diff --git a/app/assets/javascripts/admin/customers/services/customers.js.coffee b/app/assets/javascripts/admin/customers/services/customers.js.coffee deleted file mode 100644 index 9acfa317d2..0000000000 --- a/app/assets/javascripts/admin/customers/services/customers.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -angular.module("admin.customers").factory 'Customers', (CustomerResource) -> - new class Customers - customers: [] - customers_by_id: {} - loaded: false - - index: (params={}, callback=null) -> - CustomerResource.index params, (data) => - for customer in data - @customers.push customer - @customers_by_id[customer.id] = customer - - @loaded = true - (callback || angular.noop)(@customers) - - @customers diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee index 7b9a8165a1..45ca04bfe7 100644 --- a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee @@ -5,11 +5,11 @@ angular.module("admin.enterprise_groups") $scope.menu.setItems [ { name: 'Primary Details', icon_class: "icon-user" } - { name: 'Users', icon_class: "icon-user" } - { name: 'About', icon_class: "icon-pencil" } - { name: 'Images', icon_class: "icon-picture" } - { name: 'Contact', icon_class: "icon-phone" } - { name: 'Web', icon_class: "icon-globe" } + { name: (t('users')), icon_class: "icon-user" } + { name: (t('about')), icon_class: "icon-pencil" } + { name: (t('images')), icon_class: "icon-picture" } + { name: (t('contact')), icon_class: "icon-phone" } + { name: (t('web')), icon_class: "icon-globe" } ] $scope.select(0) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index d48f2a42f8..a806691465 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -6,7 +6,7 @@ angular.module("admin.enterprises") $scope.navClear = NavigationCheck.clear $scope.pristineEmail = $scope.Enterprise.email $scope.menu = SideMenu - $scope.newManager = { id: '', email: 'Add a manager...' } + $scope.newManager = { id: '', email: (t('add_manager')) } # Provide a callback for generating warning messages displayed before leaving the page. This is passed in # from a directive "nav-check" in the page - if we pass it here it will be called in the test suite, @@ -31,4 +31,4 @@ angular.module("admin.enterprises") if (user for user in $scope.Enterprise.users when user.id == manager.id).length == 0 $scope.Enterprise.users.push manager else - alert "#{manager.email} is already a manager!" + alert ("#{manager.email}" + " " + t("is_already_manager")) 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 d464585f67..a0105fefa4 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -5,20 +5,21 @@ angular.module("admin.enterprises") $scope.select = SideMenu.select $scope.menu.setItems [ - { name: 'Primary Details', icon_class: "icon-home" } - { name: 'Users', icon_class: "icon-user" } - { name: 'Address', icon_class: "icon-map-marker" } - { name: 'Contact', icon_class: "icon-phone" } - { name: 'Social', icon_class: "icon-twitter" } - { name: 'About', icon_class: "icon-pencil" } - { name: 'Business Details', icon_class: "icon-briefcase" } - { name: 'Images', icon_class: "icon-picture" } - { name: "Properties", icon_class: "icon-tags", show: "showProperties()" } - { 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()" } + { name: t('primary_details'), icon_class: "icon-home" } + { name: t('users'), icon_class: "icon-user" } + { name: t('address'), icon_class: "icon-map-marker" } + { name: t('contact'), icon_class: "icon-phone" } + { name: t('social'), icon_class: "icon-twitter" } + { name: t('about'), icon_class: "icon-pencil" } + { name: t('business_details'), icon_class: "icon-briefcase" } + { name: t('images'), icon_class: "icon-picture" } + { name: t('properties'), icon_class: "icon-tags", show: "showProperties()" } + { name: t('shipping_methods'), icon_class: "icon-truck", show: "showShippingMethods()" } + { name: t('payment_methods'), icon_class: "icon-money", show: "showPaymentMethods()" } + { name: t('enterprise_fees'), icon_class: "icon-tasks", show: "showEnterpriseFees()" } + { name: t('inventory_settings'), icon_class: "icon-list-ol", show: "enterpriseIsShop()" } + { name: t('tag_rules'), icon_class: "icon-random", show: "enterpriseIsShop()" } + { name: t('shop_preferences'), icon_class: "icon-shopping-cart", show: "enterpriseIsShop()" } ] $scope.select(0) @@ -42,8 +43,5 @@ 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.enterpriseIsShop = -> $scope.Enterprise.sells != "none" diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index 6be7e00ffa..2074a1ea05 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.dropdown', 'pasvaz.bindonce', 'ngSanitize'] ) \ No newline at end of file +angular.module("admin.enterprises", [ "admin.paymentMethods", "admin.utils", "admin.shippingMethods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.tagRules', 'admin.dropdown', 'pasvaz.bindonce', 'ngSanitize'] ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/filters/translate.js.coffee b/app/assets/javascripts/admin/filters/translate.js.coffee new file mode 100644 index 0000000000..20becc147a --- /dev/null +++ b/app/assets/javascripts/admin/filters/translate.js.coffee @@ -0,0 +1,7 @@ +angular.module('ofn.admin').filter "translate", -> + (key, options) -> + t(key, options) + +angular.module('ofn.admin').filter "t", -> + (key, options) -> + t(key, options) 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 ec454e9216..132480d987 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 @@ -15,8 +15,6 @@ angular.module("admin.indexUtils").directive "ofnSelect2", ($sanitize, $timeout) element.select2 minimumResultsForSearch: scope.minSearch || 0 data: { results: scope.data, text: scope.text } - initSelection: (element, callback) -> - callback scope.data[0] formatSelection: (item) -> item[scope.text] formatResult: (item) -> diff --git a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee index c6709eb3b4..f905d33bfe 100644 --- a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee +++ b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee @@ -5,27 +5,27 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout, $scope.confirmDelete = true $scope.startDate = formatDate daysFromToday -7 $scope.endDate = formatDate daysFromToday 1 - $scope.bulkActions = [ { name: "Delete Selected", callback: 'deleteLineItems' } ] - $scope.selectedUnitsProduct = {}; - $scope.selectedUnitsVariant = {}; + $scope.bulkActions = [ { name: t("bom_actions_delete"), callback: 'deleteLineItems' } ] + $scope.selectedUnitsProduct = {} + $scope.selectedUnitsVariant = {} $scope.sharedResource = false $scope.columns = Columns.setColumns - order_no: { name: "Order No.", visible: false } - full_name: { name: "Name", visible: true } - email: { name: "Email", visible: false } - phone: { name: "Phone", visible: false } - order_date: { name: "Order Date", visible: true } - producer: { name: "Producer", visible: true } - order_cycle: { name: "Order Cycle", visible: false } - hub: { name: "Hub", visible: false } - variant: { name: "Variant", visible: true } - quantity: { name: "Quantity", visible: true } - max: { name: "Max", visible: true } - final_weight_volume: { name: "Weight/Volume", visible: false } - price: { name: "Price", visible: false } + order_no: { name: t("bom_no"), visible: false } + full_name: { name: t("name"), visible: true } + email: { name: t("email"), visible: false } + phone: { name: t("phone"), visible: false } + order_date: { name: t("bom_date"), visible: true } + producer: { name: t("producer"), visible: true } + order_cycle: { name: t("bom_cycle"), visible: false } + hub: { name: t("bom_hub"), visible: false } + variant: { name: t("bom_variant"), visible: true } + quantity: { name: t("bom_quantity"), visible: true } + max: { name: t("bom_max"), visible: true } + final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false } + price: { name: t("price"), visible: false } $scope.confirmRefresh = -> - LineItems.allSaved() || confirm("Unsaved changes exist and will be lost if you continue.") + LineItems.allSaved() || confirm(t "unsaved_changes_warning") $scope.resetSelectFilters = -> $scope.distributorFilter = blankOption().id @@ -73,12 +73,12 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout, StatusMessage.display 'success', "All changes saved" $scope.bulk_order_form.$setPristine() ).catch -> - StatusMessage.display 'failure', "Fields with red borders contain errors." + StatusMessage.display 'failure', t "unsaved_changes_error" else - StatusMessage.display 'failure', "Fields with red borders contain errors." + StatusMessage.display 'failure', t "unsaved_changes_error" $scope.deleteLineItem = (lineItem) -> - if ($scope.confirmDelete && confirm("Are you sure?")) || !$scope.confirmDelete + if ($scope.confirmDelete && confirm(t "are_you_sure")) || !$scope.confirmDelete LineItems.delete lineItem, => $scope.lineItems.splice $scope.lineItems.indexOf(lineItem), 1 diff --git a/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee b/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee index 092fd5bbd2..c2595faa6d 100644 --- a/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.payment_methods") +angular.module("admin.paymentMethods") .controller "paymentMethodCtrl", ($scope, PaymentMethods) -> $scope.findPaymentMethodByID = (id) -> - $scope.PaymentMethod = PaymentMethods.findByID(id) \ No newline at end of file + $scope.PaymentMethod = PaymentMethods.findByID(id) diff --git a/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee b/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee index e75142ae0d..01553647d4 100644 --- a/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee @@ -1 +1 @@ -angular.module("admin.payment_methods", []) \ No newline at end of file +angular.module("admin.paymentMethods", []) diff --git a/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee b/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee index 21e557cac3..c31a20d96f 100644 --- a/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.payment_methods") +angular.module("admin.paymentMethods") .factory "PaymentMethods", (paymentMethods) -> new class PaymentMethods paymentMethods: paymentMethods diff --git a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee index dabe52574e..cc7bd4ee3e 100644 --- a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee @@ -1,4 +1,2 @@ -angular.module("admin.shipping_methods") - .controller "shippingMethodCtrl", ($scope, ShippingMethods) -> - $scope.findShippingMethodByID = (id) -> - $scope.ShippingMethod = ShippingMethods.findByID(id) \ No newline at end of file +angular.module("admin.shippingMethods").controller "shippingMethodCtrl", ($scope, shippingMethod) -> + $scope.shippingMethod = shippingMethod diff --git a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee new file mode 100644 index 0000000000..91569b2256 --- /dev/null +++ b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.shippingMethods") + .controller "shippingMethodsCtrl", ($scope, ShippingMethods) -> + $scope.findShippingMethodByID = (id) -> + $scope.ShippingMethod = ShippingMethods.findByID(id) diff --git a/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee b/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee index 556445c869..c691f5dae5 100644 --- a/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.shipping_methods") +angular.module("admin.shippingMethods") .factory "ShippingMethods", (shippingMethods) -> new class ShippingMethods shippingMethods: shippingMethods diff --git a/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee b/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee index 99aeb9566d..232eee7045 100644 --- a/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee @@ -1 +1 @@ -angular.module("admin.shipping_methods", []) \ No newline at end of file +angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils']) diff --git a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee new file mode 100644 index 0000000000..7209147b66 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee @@ -0,0 +1,48 @@ +angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, enterprise) -> + $scope.tagGroups = enterprise.tag_groups + + $scope.visibilityOptions = [ { id: "visible", name: "VISIBLE" }, { id: "hidden", name: "NOT VISIBLE" } ] + + updateRuleCounts = -> + index = 0 + for tagGroup in $scope.tagGroups + tagGroup.startIndex = index + index = index + tagGroup.rules.length + + updateRuleCounts() + + $scope.updateTagsRulesFor = (tagGroup) -> + for tagRule in tagGroup.rules + tagRule.preferred_customer_tags = (tag.text for tag in tagGroup.tags).join(",") + + $scope.addNewRuleTo = (tagGroup, ruleType) -> + newRule = + id: null + preferred_customer_tags: (tag.text for tag in tagGroup.tags).join(",") + type: "TagRule::#{ruleType}" + switch ruleType + when "DiscountOrder" + newRule.calculator = { preferred_flat_percent: 0 } + when "FilterShippingMethods" + newRule.peferred_shipping_method_tags = [] + newRule.preferred_matched_shipping_methods_visibility = "visible" + tagGroup.rules.push(newRule) + updateRuleCounts() + + $scope.addNewTag = -> + $scope.tagGroups.push { tags: [], rules: [] } + + $scope.deleteTagRule = (tagGroup, tagRule) -> + index = tagGroup.rules.indexOf(tagRule) + return unless index >= 0 + if tagRule.id is null + tagGroup.rules.splice(index, 1) + updateRuleCounts() + else + if confirm("Are you sure?") + $http + method: "DELETE" + url: "/admin/enterprises/#{enterprise.id}/tag_rules/#{tagRule.id}.json" + .success -> + tagGroup.rules.splice(index, 1) + updateRuleCounts() diff --git a/app/assets/javascripts/admin/tag_rules/directives/invert_number.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/invert_number.js.coffee new file mode 100644 index 0000000000..2412eec18c --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/invert_number.js.coffee @@ -0,0 +1,11 @@ +angular.module("admin.tagRules").directive "invertNumber", -> + restrict: "A" + require: "ngModel" + link: (scope, element, attrs, ngModel) -> + ngModel.$parsers.push (viewValue) -> + return -parseInt(viewValue) unless isNaN(parseInt(viewValue)) + viewValue + + ngModel.$formatters.push (modelValue) -> + return -parseInt(modelValue) unless isNaN(parseInt(modelValue)) + modelValue diff --git a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee new file mode 100644 index 0000000000..54e33006e4 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee @@ -0,0 +1,34 @@ +angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templateCache, $window) -> + restrict: 'A' + scope: true + link: (scope, element, attr) -> + # Compile modal template + template = $compile($templateCache.get('admin/new_tag_rule_dialog.html'))(scope) + + scope.ruleTypes = [ + # { id: "DiscountOrder", name: 'Apply a discount to orders' } + { id: "FilterShippingMethods", name: 'Show/Hide shipping methods' } + ] + + scope.ruleType = "DiscountOrder" + + # Set Dialog options + template.dialog + show: { effect: "fade", duration: 400 } + hide: { effect: "fade", duration: 300 } + autoOpen: false + resizable: false + width: $window.innerWidth * 0.4; + modal: true + open: (event, ui) -> + $('.ui-widget-overlay').bind 'click', -> + $(this).siblings('.ui-dialog').find('.ui-dialog-content').dialog('close') + + # Link opening of dialog to click event on element + element.bind 'click', (e) -> + template.dialog('open') + + scope.addRule = (tagGroup, ruleType) -> + scope.addNewRuleTo(tagGroup, ruleType) + template.dialog('close') + return diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee new file mode 100644 index 0000000000..b374f88782 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.tagRules").directive "discountOrder", -> + restrict: "E" + replace: true + templateUrl: "admin/tag_rules/discount_order.html" diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee new file mode 100644 index 0000000000..1a75cf8ff2 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.tagRules").directive "filterShippingMethods", -> + restrict: "E" + replace: true + templateUrl: "admin/tag_rules/filter_shipping_methods.html" diff --git a/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee b/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee new file mode 100644 index 0000000000..88c7734c33 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee @@ -0,0 +1 @@ +angular.module("admin.tagRules", ['ngTagsInput']) diff --git a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee new file mode 100644 index 0000000000..6ce7953608 --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee @@ -0,0 +1,15 @@ +angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> + restrict: "E" + template: "" + scope: + object: "=" + tagsAttr: "@?" + tagListAttr: "@?" + link: (scope, element, attrs) -> + $timeout -> + scope.tagsAttr ||= "tags" + scope.tagListAttr ||= "tag_list" + + watchString = "object.#{scope.tagsAttr}" + scope.$watchCollection watchString, -> + scope.object[scope.tagListAttr] = (tag.text for tag in scope.object[scope.tagsAttr]).join(",") diff --git a/app/assets/javascripts/darkswarm/controllers/distributor_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/distributor_node_controller.js.coffee new file mode 100644 index 0000000000..6705634771 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/distributor_node_controller.js.coffee @@ -0,0 +1,9 @@ +Darkswarm.controller "DistributorNodeCtrl", ($scope, HashNavigation, $anchorScroll) -> + $scope.toggle = -> + HashNavigation.toggle $scope.distributor.hash + + $scope.open = -> + HashNavigation.active($scope.distributor.hash) + + if $scope.open() + $anchorScroll() diff --git a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee index db995170dd..fa11c0e92f 100644 --- a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee @@ -8,6 +8,7 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterpris $scope.show_profiles = false $scope.filtersActive = false $scope.distanceMatchesShown = false + $scope.filterExpression = {active: true} $scope.$watch "query", (query)-> @@ -44,8 +45,8 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterpris $scope.filterEnterprises = -> es = Enterprises.hubs $scope.nameMatches = enterpriseMatchesNameQueryFilter(es, true) - $scope.distanceMatches = enterpriseMatchesNameQueryFilter(es, false) - $scope.distanceMatches = distanceWithinKmFilter($scope.distanceMatches, 50) + noNameMatches = enterpriseMatchesNameQueryFilter(es, false) + $scope.distanceMatches = distanceWithinKmFilter(noNameMatches, 50) $scope.updateVisibleMatches = -> @@ -65,3 +66,9 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterpris $scope.nameMatchesFiltered[0] else undefined + + $scope.showClosedShops = -> + delete $scope.filterExpression['active'] + + $scope.hideClosedShops = -> + $scope.filterExpression['active'] = true diff --git a/app/assets/javascripts/darkswarm/controllers/orders_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/orders_controller.js.coffee new file mode 100644 index 0000000000..116a2b6fd8 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/orders_controller.js.coffee @@ -0,0 +1,2 @@ +Darkswarm.controller "OrdersCtrl", ($scope, $rootScope, $timeout, Orders, Search, $document, HashNavigation, FilterSelectorsService, EnterpriseModal, enterpriseMatchesNameQueryFilter, distanceWithinKmFilter) -> + $scope.Orders = Orders diff --git a/app/assets/javascripts/darkswarm/directives/auth.js.coffee b/app/assets/javascripts/darkswarm/directives/auth.js.coffee new file mode 100644 index 0000000000..46ae301651 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/auth.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.directive 'auth', (AuthenticationService) -> + restrict: 'A' + link: (scope, elem, attrs) -> + elem.bind "click", -> + AuthenticationService.open '/' + attrs.auth diff --git a/app/assets/javascripts/darkswarm/directives/logo_fallback.js.coffee b/app/assets/javascripts/darkswarm/directives/logo_fallback.js.coffee new file mode 100644 index 0000000000..85f697af3d --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/logo_fallback.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.directive "logoFallback", () -> + restrict: "A" + link: (scope, elm, attr)-> + elm.bind('error', -> + elm.replaceWith("") + ) diff --git a/app/assets/javascripts/darkswarm/filters/format_balance.js.coffee b/app/assets/javascripts/darkswarm/filters/format_balance.js.coffee new file mode 100644 index 0000000000..1b42e9e75d --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/format_balance.js.coffee @@ -0,0 +1,7 @@ +Darkswarm.filter "formatBalance", (localizeCurrencyFilter, tFilter)-> + # Convert number to string currency using injected currency configuration. + (balance) -> + if balance < 0 + tFilter('credit') + ": " + localizeCurrencyFilter(Math.abs(balance)) + else + tFilter('balance_due') + ": " + localizeCurrencyFilter(Math.abs(balance)) diff --git a/app/assets/javascripts/darkswarm/i18n.translate.js.coffee b/app/assets/javascripts/darkswarm/i18n.translate.js.coffee index c455b6d9e1..eda092da83 100644 --- a/app/assets/javascripts/darkswarm/i18n.translate.js.coffee +++ b/app/assets/javascripts/darkswarm/i18n.translate.js.coffee @@ -4,8 +4,13 @@ window.translate = (key, options = {}) -> unless 'I18n' of window console.log 'The I18n object is undefined. Cannot translate text.' return key - return key unless key of I18n - text = I18n[key] + dict = I18n + parts = key.split '.' + while (parts.length) + part = parts.shift() + return key unless part of dict + dict = dict[part] + text = dict for name, value of options text = text.split("%{#{name}}").join(value) text diff --git a/app/assets/javascripts/darkswarm/services/map.js.coffee b/app/assets/javascripts/darkswarm/services/map.js.coffee index 96768e8379..f4bad04acc 100644 --- a/app/assets/javascripts/darkswarm/services/map.js.coffee +++ b/app/assets/javascripts/darkswarm/services/map.js.coffee @@ -2,6 +2,8 @@ Darkswarm.factory "OfnMap", (Enterprises, EnterpriseModal, visibleFilter) -> new class OfnMap constructor: -> @enterprises = @enterprise_markers(Enterprises.enterprises) + @enterprises = @enterprises.filter (enterprise) -> + enterprise.latitude != null || enterprise.longitude != null # Remove enterprises w/o lat or long enterprise_markers: (enterprises) -> @extend(enterprise) for enterprise in visibleFilter(enterprises) diff --git a/app/assets/javascripts/darkswarm/services/orders.js.coffee b/app/assets/javascripts/darkswarm/services/orders.js.coffee new file mode 100644 index 0000000000..78ba65f79e --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/orders.js.coffee @@ -0,0 +1,16 @@ +Darkswarm.factory 'Orders', (orders_by_distributor, currencyConfig, CurrentHub, Taxons, Dereferencer, visibleFilter, Matcher, Geo, $rootScope)-> + new class Orders + constructor: -> + # Populate Orders.orders from json in page. + @orders_by_distributor = orders_by_distributor + @currency_symbol = currencyConfig.symbol + + for distributor in @orders_by_distributor + @updateRunningBalance(distributor.distributed_orders) + + + updateRunningBalance: (orders) -> + for order, i in orders + balances = orders.slice(i,orders.length).map (o) -> parseFloat(o.outstanding_balance) + running_balance = balances.reduce (a,b) -> a+b + order.running_balance = running_balance.toFixed(2) diff --git a/app/assets/javascripts/templates/admin/new_tag_rule_dialog.html.haml b/app/assets/javascripts/templates/admin/new_tag_rule_dialog.html.haml new file mode 100644 index 0000000000..653e0d175d --- /dev/null +++ b/app/assets/javascripts/templates/admin/new_tag_rule_dialog.html.haml @@ -0,0 +1,10 @@ +#new-tag-rule-dialog + .text-normal.margin-bottom-30.text-center + Select a rule type: + + .text-center.margin-bottom-30 + -# %select.fullwidth{ 'select2-min-search' => 5, 'ng-model' => 'newRuleType', 'ng-options' => 'ruleType.id as ruleType.name for ruleType in availableRuleTypes' } + %input.ofn-select2.fullwidth{ :id => 'rule_type_selector', ng: { model: "ruleType" }, data: "ruleTypes", 'min-search' => "5" } + + .text-center + %input.button.red.icon-plus{ type: 'button', value: "Add Rule", ng: { click: 'addRule(tagGroup, ruleType)' } } diff --git a/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml b/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml new file mode 100644 index 0000000000..358d9ce1a6 --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml @@ -0,0 +1,37 @@ +%div + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", + ng: { value: "rule.id" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", + value: "TagRule::DiscountOrder" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_type]", + value: "Spree::Calculator::FlatPercentItemTotal" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][id]", + ng: { value: "rule.calculator.id" } } + + %input{ type: "hidden", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][preferred_flat_percent]", + ng: { value: "rule.calculator.preferred_flat_percent" } } + + %span.text-normal {{ $index + 1 }}. Orders are discounted by + %input{ type: "number", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_preferred_flat_percent", + min: -100, + max: 100, + ng: { model: "rule.calculator.preferred_flat_percent" }, 'invert-number' => true } + %span.text-normal % diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml new file mode 100644 index 0000000000..6552d834b5 --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml @@ -0,0 +1,27 @@ +%div + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", + ng: { value: "rule.id" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", + value: "TagRule::FilterShippingMethods" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_shipping_method_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_shipping_method_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %span.text-normal {{ $index + 1 }}. Shipping methods with matching tags are + %input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]", + ng: { model: "rule.preferred_matched_shipping_methods_visibility"}, + data: 'visibilityOptions', "min-search" => 5 } + -# %tags-with-translation{ object: "rule", "tags-attr" => "shipping_method_tags", "tag-list-attr" => "preferred_shipping_method_tags" } diff --git a/app/assets/stylesheets/admin/components/jquery_dialog.scss b/app/assets/stylesheets/admin/components/jquery_dialog.scss new file mode 100644 index 0000000000..2e36db6e33 --- /dev/null +++ b/app/assets/stylesheets/admin/components/jquery_dialog.scss @@ -0,0 +1,88 @@ +/** +Main colors: +dark: #545454 +light: #ccc +*/ +.ui-dialog { + border: 2px solid #4a4a4a; + border-radius:3px; + padding:0px; + -moz-box-shadow: 3px 3px 4px #797979; + -webkit-box-shadow: 3px 3px 4px #797979; + box-shadow: 3px 3px 4px #797979; + + /* For IE 8 */ + -ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#545454')"; + + /* For IE 5.5 - 7 */ + filter: progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#545454'); +} + +.ui-dialog .ui-dialog-titlebar{ + border-radius: 3px; +} + +.ui-dialog .ui-state-hover { + &.ui-dialog-titlebar-close{ + + } +} + +/*.ui-dialog .ui-icon-closethick{background:url(/static/assets/dialogCloseButton.png);}*/ + +.ui-dialog .ui-widget-header{ + background-image: none; + background-color: #ffffff; + border:0px; + border-radius: 3px; + padding: 0px 5px 0px 5px; +} +.ui-dialog .ui-widget-content{ + border: none; + border-radius: 3px; + padding: 0px 50px 30px 50px; +} + +.ui-dialog .ui-corner-all{ + border-radius:0px; +} +.ui-dialog { + .ui-state-hover, .ui-state-focus{ + border: none; + background: none; + color: #545454; + } +} + +.ui-state-hover, .ui-widget-header .ui-state-hover, .ui-widget-content .ui-state-hover { + background-color: #ffffff; + background: none; +} + +.ui-dialog-titlebar-close { + float: right; + &:before { + color: #000000; + font-size: 2em; + font-weight: 400; + content: '\00d7'; + display: inline; + } + + &:hover { + &:before { + color: #da5354; + } + } + + .ui-icon { + &.ui-icon-closethick { + display: none; + } + } +} + +.ui-widget-overlay { + background: #e9e9e9; + opacity: 0.6; +} diff --git a/app/assets/stylesheets/admin/offsets.css.scss b/app/assets/stylesheets/admin/offsets.css.scss index 762b7469f6..190ed49243 100644 --- a/app/assets/stylesheets/admin/offsets.css.scss +++ b/app/assets/stylesheets/admin/offsets.css.scss @@ -2,6 +2,10 @@ margin-bottom: 20px; } +.margin-bottom-30 { + margin-bottom: 30px; +} + .margin-bottom-50 { margin-bottom: 50px; } diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 544abfa899..761ccbc014 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -69,4 +69,23 @@ div#group_buy_calculation { text-indent:1em; } } + + &.after { + + span { + position: absolute; + transform: translate(0,-55%); + top:50%; + right: 0.5em; + pointer-events:none; + } + + input { + padding-right: 1.2em; + } + } +} + +th.actions { + white-space: nowrap; } diff --git a/app/assets/stylesheets/admin/select2.css.scss b/app/assets/stylesheets/admin/select2.css.scss index daba11d099..f94515a54c 100644 --- a/app/assets/stylesheets/admin/select2.css.scss +++ b/app/assets/stylesheets/admin/select2.css.scss @@ -1,5 +1,8 @@ .select2-container { .select2-choice { + .select2-search-choice-close { + display: none; + } .select2-arrow { width: 22px; border: none; @@ -7,4 +10,36 @@ background-color: transparent; } } + + &.light { + .select2-choice{ + background-color: #ffffff; + font-weight: normal; + border: 1px solid #5498da !important; + color: #5498da !important; + + .select2-arrow { + &:before { + color: #5498da; + font-size: 1rem; + font-weight: 400; + content: '\25be'; + display: inline; + } + } + } + + &:hover, &.select2-container-active { + .select2-choice{ + color: #ffffff !important; + background-color: #5498da !important; + + .select2-arrow { + &:before { + color: #ffffff; + } + } + } + } + } } diff --git a/app/assets/stylesheets/admin/tag_rules.css.scss b/app/assets/stylesheets/admin/tag_rules.css.scss new file mode 100644 index 0000000000..028ad6ce1c --- /dev/null +++ b/app/assets/stylesheets/admin/tag_rules.css.scss @@ -0,0 +1,68 @@ +.no_tags { + margin-bottom: 40px; + color: #aeaeae; + font-size: 1rem; + font-weight: bold; +} + +.customer_tag { + border: 1px solid #cee1f4; + margin-bottom: 40px; + + .header { + padding: 8px 10px; + background-color: #eff5fc; + border-bottom: 1px solid #cee1f4; + + table { + padding: 0px; + margin: 0px 0px 0px 0px; + tr { + td { + border: none; + } + } + } + } + + .no_rules { + padding: 8px 10px; + margin-bottom: 10px; + color: #aeaeae; + font-size: 1rem; + font-weight: bold; + } + + table { + padding: 0px; + margin: 0px 0px 10px 0px; + + tr.tag_rule { + border: none; + padding: 0px; + margin: 0px; + + td { + border: none; + padding: 4px 10px 10px 10px; + margin: 0px; + + input { + width: auto; + } + } + } + } + + .add_rule { + padding: 8px 10px; + margin-bottom: 10px; + } +} + +#new-tag-rule-dialog{ + .select2-chosen, .select2-result-label{ + font-size: 1rem; + font-weight: lighter; + } +} diff --git a/app/assets/stylesheets/admin/typography.css.scss b/app/assets/stylesheets/admin/typography.css.scss index 20148df3f1..761058fb1d 100644 --- a/app/assets/stylesheets/admin/typography.css.scss +++ b/app/assets/stylesheets/admin/typography.css.scss @@ -7,3 +7,12 @@ font-size: 1.2rem; font-weight: 300; } + +.text-red { + color: #DA5354; +} + + +input.text-big { + font-size: 1.1rem; +} diff --git a/app/assets/stylesheets/darkswarm/account.css.sass b/app/assets/stylesheets/darkswarm/account.css.sass new file mode 100644 index 0000000000..f582545b74 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/account.css.sass @@ -0,0 +1,63 @@ +@import branding +@import mixins + +.orders + @include sidepaddingSm + @include panepadding + padding-top: 10px + + h3 + padding-top: 2em + + a + color: $clr-brick + &:hover, &:active, &:focus + color: $clr-brick-med-bright + + img + display: block + width: 80px + height: auto + + i.ofn-i_059-producer, i.ofn-i_060-producer-reversed + font-size: 3rem + display: inline-block + margin-right: 0.25rem + float: left + color: $clr-turquoise + + .credit + color: green + + .debit + color: $clr-brick + + .distributor-balance.paid + visibility: hidden + + .transaction-group + + + table + border-radius: 0.5em 0.5em 0 0 + tr:nth-of-type(even) + background: transparent // clear previous + tbody.odd + tr + background-color: #f9f9f9 + border: none + // Column widths for order table + .order1 + width: 20% + .order2 + width: 20% + .order3 + width: 20% + .order4 + width: 10% + .order5 + width: 10% + .order6 + width: 10% + .order7 + width: 10% diff --git a/app/assets/stylesheets/darkswarm/footer.sass b/app/assets/stylesheets/darkswarm/footer.sass index 76dd0f4384..a0d2244c0e 100644 --- a/app/assets/stylesheets/darkswarm/footer.sass +++ b/app/assets/stylesheets/darkswarm/footer.sass @@ -30,7 +30,7 @@ footer background-color: transparent border: none padding: 0 - a.big-alert + a.alert-cta @include csstrans width: 100% border: 1px solid rgba($dark-grey, 0.35) diff --git a/app/assets/stylesheets/darkswarm/hubs.css.sass b/app/assets/stylesheets/darkswarm/hubs.css.sass index a351170d99..5946d6ee77 100644 --- a/app/assets/stylesheets/darkswarm/hubs.css.sass +++ b/app/assets/stylesheets/darkswarm/hubs.css.sass @@ -7,4 +7,7 @@ @include sidepaddingSm .name-matches, .distance-matches - margin-top: 4em \ No newline at end of file + margin-top: 4em + + .more-controls + text-align: center diff --git a/app/assets/stylesheets/darkswarm/producers.css.sass b/app/assets/stylesheets/darkswarm/producers.css.sass index 2055a45035..dce972629b 100644 --- a/app/assets/stylesheets/darkswarm/producers.css.sass +++ b/app/assets/stylesheets/darkswarm/producers.css.sass @@ -15,4 +15,4 @@ color: $clr-turquoise-bright a.button.primary &:hover, &:active, &:focus - color: white \ No newline at end of file + color: white diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 5422c4463a..eab074369e 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -84,9 +84,11 @@ padding-right: 0rem font-size: 0.8rem - .shopfront_message, .shopfront_closed_message + .shopfront_message, .shopfront_closed_message, .shopfront_hidden_message padding: 15px border-radius: 5px + + .shopfront_message, .shopfront_closed_message border: 2px solid #eb4c46 .shopfront_message @@ -94,3 +96,7 @@ .shopfront_closed_message margin: 2em 0em + + .shopfront_hidden_message + border: 2px solid #db4 + margin: 2em 0em diff --git a/app/controllers/admin/cache_settings_controller.rb b/app/controllers/admin/cache_settings_controller.rb new file mode 100644 index 0000000000..138bbc7b18 --- /dev/null +++ b/app/controllers/admin/cache_settings_controller.rb @@ -0,0 +1,13 @@ +require 'open_food_network/products_cache_integrity_checker' + +class Admin::CacheSettingsController < Spree::Admin::BaseController + + def show + @results = Exchange.cachable.map do |exchange| + checker = OpenFoodNetwork::ProductsCacheIntegrityChecker.new(exchange.receiver, exchange.order_cycle) + + {distributor: exchange.receiver, order_cycle: exchange.order_cycle, status: checker.ok?, diff: checker.diff} + end + end + +end diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index b1ceb88c2f..bd865c8130 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -7,13 +7,25 @@ module Admin respond_to do |format| format.html format.json do - render json: ActiveModel::ArraySerializer.new( @collection, - each_serializer: Api::Admin::CustomerSerializer, spree_current_user: spree_current_user - ).to_json + serialised = ActiveModel::ArraySerializer.new( + @collection, + each_serializer: Api::Admin::CustomerSerializer, + spree_current_user: spree_current_user) + render json: serialised.to_json end end end + def create + @customer = Customer.new(params[:customer]) + if user_can_create_customer? + @customer.save + render json: Api::Admin::CustomerSerializer.new(@customer).to_json + else + redirect_to '/unauthorized' + end + end + private def collection @@ -25,5 +37,10 @@ module Admin def load_managed_shops @shops = Enterprise.managed_by(spree_current_user).is_distributor end + + def user_can_create_customer? + spree_current_user.admin? || + spree_current_user.enterprises.include?(@customer.enterprise) + end end end diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 55eaabd7d8..377186b604 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -4,7 +4,7 @@ module Admin class EnterprisesController < ResourceController before_filter :load_enterprise_set, :only => :index before_filter :load_countries, :except => [:index, :register, :check_permalink] - before_filter :load_methods_and_fees, :only => [:new, :edit, :update, :create] + before_filter :load_methods_and_fees, :only => [:edit, :update] before_filter :load_groups, :only => [:new, :edit, :update, :create] before_filter :load_taxons, :only => [:new, :edit, :update, :create] before_filter :check_can_change_sells, only: :update @@ -35,6 +35,8 @@ module Admin def update invoke_callbacks(:update, :before) + tag_rules_attributes = params[object_name].delete :tag_rules_attributes + update_tag_rules(tag_rules_attributes) if tag_rules_attributes.present? if @object.update_attributes(params[object_name]) invoke_callbacks(:update, :after) flash[:success] = flash_message_for(@object, :successfully_updated) @@ -180,6 +182,27 @@ module Admin @taxons = Spree::Taxon.order(:name) end + def update_tag_rules(tag_rules_attributes) + # Due to the combination of trying to use nested attributes and type inheritance + # we cannot apply all attributes to tag rules in one hit because mass assignment + # methods that are specific to each class do not become available until after the + # record is persisted. This problem is compounded by the use of calculators. + @object.transaction do + tag_rules_attributes.select{ |i, attrs| attrs[:type].present? }.each do |i, attrs| + rule = @object.tag_rules.find_by_id(attrs.delete :id) || attrs[:type].constantize.new(enterprise: @object) + create_calculator_for(rule, attrs) if rule.type == "TagRule::DiscountOrder" && rule.calculator.nil? + rule.update_attributes(attrs) + end + end + end + + def create_calculator_for(rule, attrs) + if attrs[:calculator_type].present? && attrs[:calculator_attributes].present? + rule.update_attributes(calculator_type: attrs[:calculator_type]) + attrs[:calculator_attributes].merge!( { id: rule.calculator.id } ) + end + end + def check_can_change_bulk_sells unless spree_current_user.admin? params[:enterprise_set][:collection_attributes].each do |i, enterprise_params| diff --git a/app/controllers/admin/tag_rules_controller.rb b/app/controllers/admin/tag_rules_controller.rb new file mode 100644 index 0000000000..7d60cb4888 --- /dev/null +++ b/app/controllers/admin/tag_rules_controller.rb @@ -0,0 +1,10 @@ +module Admin + class TagRulesController < ResourceController + + respond_to :json + + respond_override destroy: { json: { + success: lambda { render nothing: true, :status => 204 } + } } + end +end diff --git a/app/controllers/api/statuses_controller.rb b/app/controllers/api/statuses_controller.rb new file mode 100644 index 0000000000..c8844b868b --- /dev/null +++ b/app/controllers/api/statuses_controller.rb @@ -0,0 +1,17 @@ +module Api + class StatusesController < BaseController + respond_to :json + + def job_queue + render json: {alive: job_queue_alive?} + end + + + private + + def job_queue_alive? + Spree::Config.last_job_queue_heartbeat_at.present? && + Time.parse(Spree::Config.last_job_queue_heartbeat_at) > 6.minutes.ago + end + end +end diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 8d7b9ab274..55b6bcc02b 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -1,4 +1,4 @@ -require 'open_food_network/products_renderer' +require 'open_food_network/cached_products_renderer' class ShopController < BaseController layout "darkswarm" @@ -11,11 +11,11 @@ class ShopController < BaseController def products begin - products_json = OpenFoodNetwork::ProductsRenderer.new(current_distributor, current_order_cycle).products + products_json = OpenFoodNetwork::CachedProductsRenderer.new(current_distributor, current_order_cycle).products_json render json: products_json - rescue OpenFoodNetwork::ProductsRenderer::NoProducts + rescue OpenFoodNetwork::CachedProductsRenderer::NoProducts render status: 404, json: '' end end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 511eced707..0c032eaa9e 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -20,11 +20,15 @@ module Admin end def admin_inject_payment_methods - admin_inject_json_ams_array "admin.payment_methods", "paymentMethods", @payment_methods, Api::Admin::IdNameSerializer + admin_inject_json_ams_array "admin.paymentMethods", "paymentMethods", @payment_methods, Api::Admin::IdNameSerializer end def admin_inject_shipping_methods - admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer + admin_inject_json_ams_array "admin.shippingMethods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer + end + + def admin_inject_shipping_method + admin_inject_json_ams "admin.shippingMethods", "shippingMethod", @shipping_method, Api::Admin::ShippingMethodSerializer end def admin_inject_shops(ngModule='admin.customers') diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index 9f6810f34f..93b7c96f95 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -4,7 +4,11 @@ module EnterprisesHelper end def available_shipping_methods - current_distributor.shipping_methods.uniq + shipping_methods = current_distributor.shipping_methods + if current_distributor.present? + current_distributor.apply_tag_rules_to(shipping_methods, customer: current_order.customer) + end + shipping_methods.uniq end def managed_enterprises diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index df67261fd7..c6be5dc63e 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -2,7 +2,7 @@ require 'open_food_network/enterprise_injection_data' module InjectionHelper def inject_enterprises - inject_json_ams "enterprises", Enterprise.activated.includes(:address).all, Api::EnterpriseSerializer, enterprise_injection_data + inject_json_ams "enterprises", Enterprise.activated.includes(address: :state).all, Api::EnterpriseSerializer, enterprise_injection_data end def inject_group_enterprises @@ -51,6 +51,11 @@ module InjectionHelper render partial: "json/injection_ams", locals: {name: 'enterpriseAttributes', json: "#{@enterprise_attributes.to_json}"} end + def inject_orders_by_distributor + data_array = spree_current_user.orders_by_distributor + inject_json_ams "orders_by_distributor", data_array, Api::OrdersByDistributorSerializer + end + def inject_json(name, partial, opts = {}) render partial: "json/injection", locals: {name: name, partial: partial}.merge(opts) end diff --git a/app/helpers/shop_helper.rb b/app/helpers/shop_helper.rb index 3066bfbe05..920d41d65e 100644 --- a/app/helpers/shop_helper.rb +++ b/app/helpers/shop_helper.rb @@ -7,4 +7,16 @@ module ShopHelper ] end end + + def require_customer? + current_distributor.require_login? && !user_is_related_to_distributor? + end + + def user_is_related_to_distributor? + spree_current_user.present? && ( + spree_current_user.admin? || + spree_current_user.enterprises.include?(current_distributor) || + spree_current_user.customer_of(current_distributor) + ) + end end diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index bbc184d800..aaed402a6f 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -11,11 +11,17 @@ module Spree end def report_payment_method_options(orders) - orders.map { |o| o.payments.first.payment_method.andand.name }.uniq + orders.map do |o| + pm = o.payments.first.payment_method + [pm.andand.name, pm.andand.id] + end.uniq end def report_shipping_method_options(orders) - orders.map { |o| o.shipping_method.andand.name }.uniq + orders.map do |o| + sm = o.shipping_method + [sm.andand.name, sm.andand.id] + end.uniq end def xero_report_types diff --git a/app/jobs/heartbeat_job.rb b/app/jobs/heartbeat_job.rb new file mode 100644 index 0000000000..93e835905f --- /dev/null +++ b/app/jobs/heartbeat_job.rb @@ -0,0 +1,5 @@ +class HeartbeatJob + def perform + Spree::Config.last_job_queue_heartbeat_at = Time.now + end +end diff --git a/app/jobs/products_cache_integrity_checker_job.rb b/app/jobs/products_cache_integrity_checker_job.rb new file mode 100644 index 0000000000..78feb3a1b0 --- /dev/null +++ b/app/jobs/products_cache_integrity_checker_job.rb @@ -0,0 +1,24 @@ +require 'open_food_network/products_cache_integrity_checker' + +ProductsCacheIntegrityCheckerJob = Struct.new(:distributor_id, :order_cycle_id) do + def perform + unless checker.ok? + Bugsnag.notify RuntimeError.new("Products JSON differs from cached version for distributor: #{distributor_id}, order cycle: #{order_cycle_id}"), diff: checker.diff.to_s(:text) + end + end + + + private + + def checker + OpenFoodNetwork::ProductsCacheIntegrityChecker.new(distributor, order_cycle) + end + + def distributor + Enterprise.find distributor_id + end + + def order_cycle + OrderCycle.find order_cycle_id + end +end diff --git a/app/jobs/refresh_products_cache_job.rb b/app/jobs/refresh_products_cache_job.rb new file mode 100644 index 0000000000..0c66925be8 --- /dev/null +++ b/app/jobs/refresh_products_cache_job.rb @@ -0,0 +1,16 @@ +require 'open_food_network/products_renderer' + +RefreshProductsCacheJob = Struct.new(:distributor_id, :order_cycle_id) do + def perform + Rails.cache.write "products-json-#{distributor_id}-#{order_cycle_id}", products_json + end + + + private + + def products_json + distributor = Enterprise.find distributor_id + order_cycle = OrderCycle.find order_cycle_id + OpenFoodNetwork::ProductsRenderer.new(distributor, order_cycle).products_json + end +end diff --git a/app/models/content_configuration.rb b/app/models/content_configuration.rb index 2357f8dfa3..e20279f5ee 100644 --- a/app/models/content_configuration.rb +++ b/app/models/content_configuration.rb @@ -7,14 +7,14 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration preference :logo, :file preference :logo_mobile, :file preference :logo_mobile_svg, :file - has_attached_file :logo + has_attached_file :logo, default_url: "/assets/ofn-logo.png" has_attached_file :logo_mobile - has_attached_file :logo_mobile_svg + has_attached_file :logo_mobile_svg, default_url: "/assets/ofn-logo-mobile.svg" # Home page preference :home_hero, :file preference :home_show_stats, :boolean, default: true - has_attached_file :home_hero + has_attached_file :home_hero, default_url: "/assets/home/home.jpg" # Producer sign-up page preference :producer_signup_pricing_table_html, :text, default: "(TODO: Pricing table)" @@ -33,7 +33,7 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration # Footer preference :footer_logo, :file - has_attached_file :footer_logo + has_attached_file :footer_logo, default_url: "/assets/ofn-logo-footer.png" preference :footer_facebook_url, :string, default: "https://www.facebook.com/OpenFoodNet" preference :footer_twitter_url, :string, default: "https://twitter.com/OpenFoodNet" preference :footer_instagram_url, :string, default: "" diff --git a/app/models/coordinator_fee.rb b/app/models/coordinator_fee.rb new file mode 100644 index 0000000000..135ee821a9 --- /dev/null +++ b/app/models/coordinator_fee.rb @@ -0,0 +1,15 @@ +class CoordinatorFee < ActiveRecord::Base + belongs_to :order_cycle + belongs_to :enterprise_fee + + after_save :refresh_products_cache + after_destroy :refresh_products_cache + + + private + + def refresh_products_cache + order_cycle.refresh_products_cache + end + +end diff --git a/app/models/customer.rb b/app/models/customer.rb index 856f7e94d7..34f62a6aa6 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -2,10 +2,12 @@ class Customer < ActiveRecord::Base acts_as_taggable belongs_to :enterprise - belongs_to :user, :class_name => Spree.user_class + belongs_to :user, class_name: Spree.user_class + + before_validation :downcase_email validates :code, uniqueness: { scope: :enterprise_id, allow_blank: true, allow_nil: true } - validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: "is associated with an existing customer" } + validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: I18n.t('validation_msg_is_associated_with_an_exising_customer') } validates :enterprise_id, presence: true scope :of, ->(enterprise) { where(enterprise_id: enterprise) } @@ -14,6 +16,10 @@ class Customer < ActiveRecord::Base private + def downcase_email + email.andand.downcase! + end + def associate_user self.user = user || Spree::User.find_by_email(email) end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index ebe4d6289a..9f839f9d08 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -41,11 +41,13 @@ class Enterprise < ActiveRecord::Base has_many :customers has_many :billable_periods has_many :inventory_items + has_many :tag_rules delegate :latitude, :longitude, :city, :state_name, :to => :address accepts_nested_attributes_for :address accepts_nested_attributes_for :producer_properties, allow_destroy: true, reject_if: lambda { |pp| pp[:property_name].blank? } + accepts_nested_attributes_for :tag_rules, allow_destroy: true, reject_if: lambda { |tag_rule| tag_rule[:preferred_customer_tags].blank? } has_attached_file :logo, styles: { medium: "300x300>", small: "180x180>", thumb: "100x100>" }, @@ -175,17 +177,6 @@ class Enterprise < ActiveRecord::Base end } - def self.find_near(suburb) - enterprises = [] - - unless suburb.nil? - addresses = Spree::Address.near([suburb.latitude, suburb.longitude], ENTERPRISE_SEARCH_RADIUS, :units => :km).joins(:enterprise).limit(10) - enterprises = addresses.collect(&:enterprise) - end - - enterprises - end - # Force a distinct count to work around relation count issue https://github.com/rails/rails/issues/5554 def self.distinct_count count(distinct: true) @@ -353,6 +344,13 @@ class Enterprise < ActiveRecord::Base abn.present? end + def apply_tag_rules_to(subject, context) + tag_rules.each do |rule| + rule.set_context(subject,context) + rule.apply + end + end + protected def devise_mailer diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index 59b2648d30..d1d0816340 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -1,11 +1,18 @@ class EnterpriseFee < ActiveRecord::Base belongs_to :enterprise belongs_to :tax_category, class_name: 'Spree::TaxCategory', foreign_key: 'tax_category_id' - has_and_belongs_to_many :order_cycles, join_table: 'coordinator_fees' + + has_many :coordinator_fees, dependent: :destroy + has_many :order_cycles, through: :coordinator_fees + has_many :exchange_fees, dependent: :destroy has_many :exchanges, through: :exchange_fees - before_destroy { order_cycles.clear } + + after_save :refresh_products_cache + # After destroy, the products cache is refreshed via the after_destroy hook for + # coordinator_fees and exchange_fees + calculated_adjustments @@ -59,6 +66,7 @@ class EnterpriseFee < ActiveRecord::Base :locked => true}, :without_protection => true) end + private def ensure_valid_tax_category_settings @@ -72,4 +80,8 @@ class EnterpriseFee < ActiveRecord::Base end return true end + + def refresh_products_cache + OpenFoodNetwork::ProductsCache.enterprise_fee_changed self + end end diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index da0996fdc5..9303cbae19 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -4,7 +4,7 @@ class EnterpriseRelationship < ActiveRecord::Base has_many :permissions, class_name: 'EnterpriseRelationshipPermission', dependent: :destroy validates_presence_of :parent_id, :child_id - validates_uniqueness_of :child_id, scope: :parent_id, message: "^That relationship is already established." + validates_uniqueness_of :child_id, scope: :parent_id, message: I18n.t('validation_msg_relationship_already_established') after_save :apply_variant_override_permissions diff --git a/app/models/exchange.rb b/app/models/exchange.rb index 137924f7ac..ad290f6c26 100644 --- a/app/models/exchange.rb +++ b/app/models/exchange.rb @@ -13,6 +13,9 @@ class Exchange < ActiveRecord::Base validates_presence_of :order_cycle, :sender, :receiver validates_uniqueness_of :sender_id, :scope => [:order_cycle_id, :receiver_id, :incoming] + after_save :refresh_products_cache + after_destroy :refresh_products_cache_from_destroy + accepts_nested_attributes_for :variants scope :in_order_cycle, lambda { |order_cycle| where(order_cycle_id: order_cycle) } @@ -31,6 +34,12 @@ class Exchange < ActiveRecord::Base joins('INNER JOIN enterprises AS receiver ON (receiver.id = exchanges.receiver_id)'). order("CASE WHEN exchanges.incoming='t' THEN sender.name ELSE receiver.name END") + # Exchanges on order cycles that are dated and are upcoming or open are cached + scope :cachable, outgoing. + joins(:order_cycle). + merge(OrderCycle.dated). + merge(OrderCycle.not_closed) + scope :managed_by, lambda { |user| if user.has_spree_role?('admin') scoped @@ -75,4 +84,11 @@ class Exchange < ActiveRecord::Base end end + def refresh_products_cache + OpenFoodNetwork::ProductsCache.exchange_changed self + end + + def refresh_products_cache_from_destroy + OpenFoodNetwork::ProductsCache.exchange_destroyed self + end end diff --git a/app/models/exchange_fee.rb b/app/models/exchange_fee.rb index ff9f12e8dd..03fce8ce04 100644 --- a/app/models/exchange_fee.rb +++ b/app/models/exchange_fee.rb @@ -1,4 +1,15 @@ class ExchangeFee < ActiveRecord::Base belongs_to :exchange belongs_to :enterprise_fee + + + after_save :refresh_products_cache + after_destroy :refresh_products_cache + + + private + + def refresh_products_cache + exchange.refresh_products_cache + end end diff --git a/app/models/inventory_item.rb b/app/models/inventory_item.rb index 2648e38341..60e1713612 100644 --- a/app/models/inventory_item.rb +++ b/app/models/inventory_item.rb @@ -1,3 +1,5 @@ +require 'open_food_network/products_cache' + class InventoryItem < ActiveRecord::Base attr_accessible :enterprise, :enterprise_id, :variant, :variant_id, :visible @@ -11,4 +13,13 @@ class InventoryItem < ActiveRecord::Base scope :visible, where(visible: true) scope :hidden, where(visible: false) + + after_save :refresh_products_cache + + + private + + def refresh_products_cache + OpenFoodNetwork::ProductsCache.inventory_item_changed self + end end diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 31a68faa37..894cbac7f7 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -1,6 +1,8 @@ class OrderCycle < ActiveRecord::Base belongs_to :coordinator, :class_name => 'Enterprise' - has_and_belongs_to_many :coordinator_fees, :class_name => 'EnterpriseFee', :join_table => 'coordinator_fees' + + has_many :coordinator_fee_refs, class_name: 'CoordinatorFee' + has_many :coordinator_fees, through: :coordinator_fee_refs, source: :enterprise_fee has_many :exchanges, :dependent => :destroy @@ -11,14 +13,18 @@ class OrderCycle < ActiveRecord::Base validates_presence_of :name, :coordinator_id + after_save :refresh_products_cache + 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) } scope :upcoming, lambda { where('order_cycles.orders_open_at > ?', Time.zone.now) } + scope :not_closed, lambda { where('order_cycles.orders_close_at > ? OR order_cycles.orders_close_at IS NULL', Time.zone.now) } scope :closed, lambda { where('order_cycles.orders_close_at < ?', Time.zone.now).order("order_cycles.orders_close_at DESC") } scope :undated, where('order_cycles.orders_open_at IS NULL OR orders_close_at IS NULL') + scope :dated, where('orders_open_at IS NOT NULL AND orders_close_at IS NOT NULL') scope :soonest_closing, lambda { active.order('order_cycles.orders_close_at ASC') } # TODO This method returns all the closed orders. So maybe we can replace it with :recently_closed. @@ -187,6 +193,10 @@ class OrderCycle < ActiveRecord::Base self.variants.include? variant end + def dated? + !undated? + end + def undated? self.orders_open_at.nil? || self.orders_close_at.nil? end @@ -236,6 +246,10 @@ class OrderCycle < ActiveRecord::Base coordinator.users.include? user end + def refresh_products_cache + OpenFoodNetwork::ProductsCache.order_cycle_changed self + end + private diff --git a/app/models/producer_property.rb b/app/models/producer_property.rb index 178e7646ff..5b9fdd6d1c 100644 --- a/app/models/producer_property.rb +++ b/app/models/producer_property.rb @@ -1,8 +1,12 @@ class ProducerProperty < ActiveRecord::Base + belongs_to :producer, class_name: 'Enterprise' belongs_to :property, class_name: 'Spree::Property' default_scope order("#{self.table_name}.position") + after_save :refresh_products_cache + after_destroy :refresh_products_cache_from_destroy + def property_name property.name if property @@ -14,4 +18,16 @@ class ProducerProperty < ActiveRecord::Base Spree::Property.create(name: name, presentation: name) end end + + + private + + def refresh_products_cache + OpenFoodNetwork::ProductsCache.producer_property_changed self + end + + def refresh_products_cache_from_destroy + OpenFoodNetwork::ProductsCache.producer_property_destroyed self + end + end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 8c93fe4b71..867d3c9e2b 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -72,6 +72,10 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty + can [:admin, :destroy], TagRule do |tag_rule| + user.enterprises.include? tag_rule.enterprise + end + can [:admin, :index, :create], Enterprise can [:read, :edit, :update, :bulk_update, :resend_confirmation], Enterprise do |enterprise| OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise @@ -97,6 +101,11 @@ class AbilityDecorator can [:print], Spree::Order do |order| order.user == user end + + can [:create], Customer + can [:destroy], Customer do |customer| + user.enterprises.include? customer.enterprise + end end def add_product_management_abilities(user) diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb index 7959b63944..840ce4f6a4 100644 --- a/app/models/spree/app_configuration_decorator.rb +++ b/app/models/spree/app_configuration_decorator.rb @@ -22,4 +22,8 @@ Spree::AppConfiguration.class_eval do preference :account_invoices_tax_rate, :decimal, default: 0 preference :shop_trial_length_days, :integer, default: 30 preference :minimum_billable_turnover, :integer, default: -1 + + # Monitoring + preference :last_job_queue_heartbeat_at, :string, default: nil + end diff --git a/app/models/spree/classification_decorator.rb b/app/models/spree/classification_decorator.rb index c69eb01fcf..5e9655a907 100644 --- a/app/models/spree/classification_decorator.rb +++ b/app/models/spree/classification_decorator.rb @@ -1,6 +1,15 @@ Spree::Classification.class_eval do belongs_to :product, :class_name => "Spree::Product", touch: true + after_save :refresh_products_cache before_destroy :dont_destroy_if_primary_taxon + after_destroy :refresh_products_cache + + + private + + def refresh_products_cache + product.refresh_products_cache + end def dont_destroy_if_primary_taxon if product.primary_taxon == taxon diff --git a/app/models/spree/image_decorator.rb b/app/models/spree/image_decorator.rb index 7d86aae3a6..24056926b7 100644 --- a/app/models/spree/image_decorator.rb +++ b/app/models/spree/image_decorator.rb @@ -1,4 +1,7 @@ Spree::Image.class_eval do + after_save :refresh_products_cache + after_destroy :refresh_products_cache + # Spree stores attachent definitions in JSON. This converts the style name and format to # strings. However, when paperclip encounters these, it doesn't recognise the format. # Here we solve that problem by converting format and style name to symbols. @@ -20,4 +23,11 @@ Spree::Image.class_eval do end reformat_styles + + + private + + def refresh_products_cache + viewable.try :refresh_products_cache + end end diff --git a/app/models/spree/option_type_decorator.rb b/app/models/spree/option_type_decorator.rb new file mode 100644 index 0000000000..aea706c847 --- /dev/null +++ b/app/models/spree/option_type_decorator.rb @@ -0,0 +1,13 @@ +module Spree + OptionType.class_eval do + has_many :products, through: :product_option_types + after_save :refresh_products_cache + + + private + + def refresh_products_cache + products(:reload).each &:refresh_products_cache + end + end +end diff --git a/app/models/spree/option_value_decorator.rb b/app/models/spree/option_value_decorator.rb new file mode 100644 index 0000000000..cfe9c23cca --- /dev/null +++ b/app/models/spree/option_value_decorator.rb @@ -0,0 +1,20 @@ +module Spree + OptionValue.class_eval do + after_save :refresh_products_cache + around_destroy :refresh_products_cache_from_destroy + + + private + + def refresh_products_cache + variants(:reload).each &:refresh_products_cache + end + + def refresh_products_cache_from_destroy + vs = variants(:reload).to_a + yield + vs.each &:refresh_products_cache + end + + end +end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 8f1c0381b1..f0e9324185 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -17,7 +17,8 @@ Spree::Order.class_eval do attr_accessible :order_cycle_id, :distributor_id before_validation :shipping_address_from_distributor - before_validation :associate_customer, unless: :customer_is_valid? + before_validation :associate_customer, unless: :customer_id? + before_validation :ensure_customer, unless: :customer_is_valid? checkout_flow do go_to_state :address @@ -69,13 +70,6 @@ Spree::Order.class_eval do where("state != ?", state) } - scope :with_payment_method_name, lambda { |payment_method_name| - joins(:payments => :payment_method). - where('spree_payment_methods.name IN (?)', payment_method_name). - select('DISTINCT spree_orders.*') - } - - # -- Methods def products_available_from_new_distribution # Check that the line_items in the current order are available from a newly selected distribution @@ -179,6 +173,10 @@ Spree::Order.class_eval do if order_cycle OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self end + + if distributor.present? && customer.present? + distributor.apply_tag_rules_to(self, customer: customer) + end end end @@ -286,17 +284,21 @@ Spree::Order.class_eval do def customer_is_valid? return true unless require_customer? - customer.present? && customer.enterprise_id == distributor_id && customer.email == (user.andand.email || email) + customer.present? && customer.enterprise_id == distributor_id && customer.email == email_for_customer + end + + def email_for_customer + (user.andand.email || email).andand.downcase end def associate_customer - email_for_customer = user.andand.email || email - existing_customer = Customer.of(distributor).find_by_email(email_for_customer) - if existing_customer - self.customer = existing_customer - else - new_customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user) - self.customer = new_customer + return customer if customer.present? + self.customer = Customer.of(distributor).find_by_email(email_for_customer) + end + + def ensure_customer + unless associate_customer + self.customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user) end end end diff --git a/app/models/spree/preference_decorator.rb b/app/models/spree/preference_decorator.rb new file mode 100644 index 0000000000..c3ab3f9a94 --- /dev/null +++ b/app/models/spree/preference_decorator.rb @@ -0,0 +1,31 @@ +require 'open_food_network/products_cache' + +module Spree + Preference.class_eval do + after_save :refresh_products_cache + + # When the setting preferred_product_selection_from_inventory_only has changed, we want to + # refresh all active exchanges for this enterprise. + def refresh_products_cache + if product_selection_from_inventory_only_changed? + OpenFoodNetwork::ProductsCache.distributor_changed(enterprise) + end + end + + + private + + def product_selection_from_inventory_only_changed? + key =~ product_selection_from_inventory_only_regex + end + + def enterprise + enterprise_id = key.match(product_selection_from_inventory_only_regex)[1] + enterprise = Enterprise.find enterprise_id + end + + def product_selection_from_inventory_only_regex + /^enterprise\/product_selection_from_inventory_only\/(\d+)$/ + end + end +end diff --git a/app/models/spree/price_decorator.rb b/app/models/spree/price_decorator.rb new file mode 100644 index 0000000000..f50a86066b --- /dev/null +++ b/app/models/spree/price_decorator.rb @@ -0,0 +1,12 @@ +module Spree + Price.class_eval do + after_save :refresh_products_cache + + + private + + def refresh_products_cache + variant.andand.refresh_products_cache + end + end +end diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index c7e1cfb68d..e6acd440c7 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -22,12 +22,10 @@ Spree::Product.class_eval do attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value attr_accessible :inherits_properties, :sku - before_validation :sanitize_permalink - # validates_presence_of :variants, unless: :new_record?, message: "Product must have at least one variant" validates_presence_of :supplier - validates :primary_taxon, presence: { message: "^Product Category can't be blank" } - validates :tax_category_id, presence: { message: "^Tax Category can't be blank" }, if: "Spree::Config.products_require_tax_category" + validates :primary_taxon, presence: { message: I18n.t("validation_msg_product_category_cant_be_blank") } + validates :tax_category_id, presence: { message: I18n.t("validation_msg_tax") }, if: "Spree::Config.products_require_tax_category" validates_presence_of :variant_unit validates_presence_of :variant_unit_scale, @@ -35,11 +33,13 @@ Spree::Product.class_eval do validates_presence_of :variant_unit_name, if: -> p { p.variant_unit == 'items' } - after_save :ensure_standard_variant after_initialize :set_available_on_to_now, :if => :new_record? - after_save :update_units - after_touch :touch_distributors + before_validation :sanitize_permalink before_save :add_primary_taxon_to_taxons + after_touch :touch_distributors + after_save :ensure_standard_variant + after_save :update_units + after_save :refresh_products_cache # -- Joins @@ -198,6 +198,11 @@ Spree::Product.class_eval do alias_method_chain :delete, :delete_from_order_cycles + def refresh_products_cache + OpenFoodNetwork::ProductsCache.product_changed self + end + + private def set_available_on_to_now diff --git a/app/models/spree/product_property_decorator.rb b/app/models/spree/product_property_decorator.rb new file mode 100644 index 0000000000..0f3329c03d --- /dev/null +++ b/app/models/spree/product_property_decorator.rb @@ -0,0 +1,10 @@ +module Spree + ProductProperty.class_eval do + after_save :refresh_products_cache + after_destroy :refresh_products_cache + + def refresh_products_cache + product.refresh_products_cache + end + end +end diff --git a/app/models/spree/property_decorator.rb b/app/models/spree/property_decorator.rb new file mode 100644 index 0000000000..5b8e53338c --- /dev/null +++ b/app/models/spree/property_decorator.rb @@ -0,0 +1,15 @@ +module Spree + Property.class_eval do + after_save :refresh_products_cache + + # When a Property is destroyed, dependent-destroy will destroy all ProductProperties, + # which will take care of refreshing the products cache + + + private + + def refresh_products_cache + product_properties(:reload).each &:refresh_products_cache + end + end +end diff --git a/app/models/spree/shipping_method_decorator.rb b/app/models/spree/shipping_method_decorator.rb index b8be603048..f4f1d2c999 100644 --- a/app/models/spree/shipping_method_decorator.rb +++ b/app/models/spree/shipping_method_decorator.rb @@ -1,10 +1,12 @@ Spree::ShippingMethod.class_eval do + acts_as_taggable + has_many :distributor_shipping_methods has_many :distributors, through: :distributor_shipping_methods, class_name: 'Enterprise', foreign_key: 'distributor_id' after_save :touch_distributors attr_accessible :distributor_ids, :description - attr_accessible :require_ship_address + attr_accessible :require_ship_address, :tag_list validates :distributors, presence: { message: "^At least one hub must be selected" } diff --git a/app/models/spree/taxon_decorator.rb b/app/models/spree/taxon_decorator.rb index 1a26ce73a8..a1e07cc53e 100644 --- a/app/models/spree/taxon_decorator.rb +++ b/app/models/spree/taxon_decorator.rb @@ -1,7 +1,12 @@ Spree::Taxon.class_eval do + has_many :classifications, :dependent => :destroy + + self.attachment_definitions[:icon][:path] = 'public/images/spree/taxons/:id/:style/:basename.:extension' self.attachment_definitions[:icon][:url] = '/images/spree/taxons/:id/:style/:basename.:extension' + after_save :refresh_products_cache + # Indicate which filters should be used for this taxon def applicable_filters @@ -45,4 +50,11 @@ Spree::Taxon.class_eval do taxons end + + + private + + def refresh_products_cache + products(:reload).each &:refresh_products_cache + end end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index b724a41d1b..be60307d52 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -15,6 +15,7 @@ Spree.user_class.class_eval do accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true attr_accessible :enterprise_ids, :enterprise_roles_attributes, :enterprise_limit + after_create :associate_customers after_create :send_signup_confirmation validate :limit_owned_enterprises @@ -41,6 +42,10 @@ Spree.user_class.class_eval do customers.of(enterprise).first end + def associate_customers + Customer.update_all({ user_id: id }, { user_id: nil, email: email }) + end + def send_signup_confirmation Delayed::Job.enqueue ConfirmSignupJob.new(id) end @@ -49,6 +54,27 @@ Spree.user_class.class_eval do owned_enterprises(:reload).size < enterprise_limit end + # Returns Enterprise IDs for distributors that the user has shopped at + def enterprises_ordered_from + orders.where(state: :complete).map(&:distributor_id).uniq + end + + # Returns orders and their associated payments for all distributors that have been ordered from + def compelete_orders_by_distributor + Enterprise + .includes(distributed_orders: { payments: :payment_method }) + .where(enterprises: { id: enterprises_ordered_from }, + spree_orders: { state: 'complete', user_id: id }) + .order('spree_orders.completed_at DESC') + end + + def orders_by_distributor + # Remove uncompleted payments as these will not be reflected in order balance + data_array = compelete_orders_by_distributor.to_a + remove_uncompleted_payments(data_array) + data_array.sort! { |a, b| b.distributed_orders.length <=> a.distributed_orders.length } + end + private def limit_owned_enterprises @@ -56,4 +82,12 @@ Spree.user_class.class_eval do errors.add(:owned_enterprises, "^#{email} is not permitted to own any more enterprises (limit is #{enterprise_limit}).") end end + + def remove_uncompleted_payments(enterprises) + enterprises.each do |enterprise| + enterprise.distributed_orders.each do |order| + order.payments.keep_if { |payment| payment.state == "completed" } + end + end + end end diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index ea04a1a5ac..21a228de19 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -1,5 +1,6 @@ require 'open_food_network/enterprise_fee_calculator' require 'open_food_network/variant_and_line_item_naming' +require 'open_food_network/products_cache' Spree::Variant.class_eval do # Remove method From Spree, so method from the naming module is used instead @@ -9,7 +10,7 @@ Spree::Variant.class_eval do include OpenFoodNetwork::VariantAndLineItemNaming - has_many :exchange_variants, dependent: :destroy + has_many :exchange_variants has_many :exchanges, through: :exchange_variants has_many :variant_overrides has_many :inventory_items @@ -25,6 +26,9 @@ Spree::Variant.class_eval do before_validation :update_weight_from_unit_value, if: -> v { v.product.present? } after_save :update_units + after_save :refresh_products_cache + around_destroy :destruction + scope :with_order_cycles_inner, joins(exchanges: :order_cycle) @@ -92,9 +96,37 @@ Spree::Variant.class_eval do end end + def refresh_products_cache + if is_master? + product.refresh_products_cache + else + OpenFoodNetwork::ProductsCache.variant_changed self + end + end + + private def update_weight_from_unit_value self.weight = weight_from_unit_value if self.product.variant_unit == 'weight' && unit_value.present? end + + def destruction + if is_master? + exchange_variants(:reload).destroy_all + yield + product.refresh_products_cache + + else + OpenFoodNetwork::ProductsCache.variant_destroyed(self) do + # Remove this association here instead of using dependent: :destroy because + # dependent-destroy acts before this around_filter is called, so ProductsCache + # has no way of knowing which exchanges the variant was a member of. + exchange_variants(:reload).destroy_all + + # Destroy the variant + yield + end + end + end end diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb new file mode 100644 index 0000000000..6e6405a589 --- /dev/null +++ b/app/models/tag_rule.rb @@ -0,0 +1,42 @@ +class TagRule < ActiveRecord::Base + attr_accessor :subject, :context + + belongs_to :enterprise + + preference :customer_tags, :string, default: "" + + validates :enterprise, presence: true + + attr_accessible :enterprise, :enterprise_id, :preferred_customer_tags + + def set_context(subject, context) + @subject = subject + @context = context + end + + def apply + if relevant? + if customer_tags_match? + apply! + else + apply_default! if respond_to?(:apply_default!,true) + end + end + end + + private + + def relevant? + return false unless subject_class_matches? + if respond_to?(:additional_requirements_met?, true) + return false unless additional_requirements_met? + end + true + end + + def customer_tags_match? + context_customer_tags = context.andand[:customer].andand.tag_list || [] + preferred_tags = preferred_customer_tags.split(",") + ( context_customer_tags & preferred_tags ).any? + end +end diff --git a/app/models/tag_rule/discount_order.rb b/app/models/tag_rule/discount_order.rb new file mode 100644 index 0000000000..5984814289 --- /dev/null +++ b/app/models/tag_rule/discount_order.rb @@ -0,0 +1,23 @@ +class TagRule::DiscountOrder < TagRule + calculated_adjustments + + private + + # Warning: this should only EVER be called via TagRule#apply + def apply! + create_adjustment(I18n.t("discount"), subject, subject) + end + + def subject_class_matches? + subject.class == Spree::Order + end + + def additional_requirements_met? + return false if already_applied? + true + end + + def already_applied? + subject.adjustments.where(originator_id: id, originator_type: "TagRule").any? + end +end diff --git a/app/models/tag_rule/filter_shipping_methods.rb b/app/models/tag_rule/filter_shipping_methods.rb new file mode 100644 index 0000000000..74438e560e --- /dev/null +++ b/app/models/tag_rule/filter_shipping_methods.rb @@ -0,0 +1,32 @@ +class TagRule::FilterShippingMethods < TagRule + preference :matched_shipping_methods_visibility, :string, default: "visible" + preference :shipping_method_tags, :string, default: "" + + attr_accessible :preferred_matched_shipping_methods_visibility, :preferred_shipping_method_tags + + private + + # Warning: this should only EVER be called via TagRule#apply + def apply! + unless preferred_matched_shipping_methods_visibility == "visible" + subject.reject!{ |sm| tags_match?(sm) } + end + end + + def apply_default! + if preferred_matched_shipping_methods_visibility == "visible" + subject.reject!{ |sm| tags_match?(sm) } + end + end + + def tags_match?(shipping_method) + shipping_method_tags = shipping_method.andand.tag_list || [] + preferred_tags = preferred_shipping_method_tags.split(",") + ( shipping_method_tags & preferred_tags ).any? + end + + def subject_class_matches? + subject.class == Array && + subject.all? { |i| i.class == Spree::ShippingMethod } + end +end diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index baede3e62d..b373900e0c 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -6,6 +6,9 @@ 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 + after_save :refresh_products_cache_from_save + after_destroy :refresh_products_cache_from_destroy + default_scope where(permission_revoked_at: nil) scope :for_hubs, lambda { |hubs| @@ -75,4 +78,11 @@ class VariantOverride < ActiveRecord::Base VariantOverride.where(variant_id: variant, hub_id: hub).first end + def refresh_products_cache_from_save + OpenFoodNetwork::ProductsCache.variant_override_changed self + end + + def refresh_products_cache_from_destroy + OpenFoodNetwork::ProductsCache.variant_override_destroyed self + end end diff --git a/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface b/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface index 14f4925206..37754bb40e 100644 --- a/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface +++ b/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface @@ -1,4 +1,4 @@ // insert_bottom "[data-hook='admin_configurations_sidebar_menu']" %li - = link_to 'Accounts & Billing', main_app.edit_admin_accounts_and_billing_settings_path + = link_to t(:accounts_and_billing), main_app.edit_admin_accounts_and_billing_settings_path diff --git a/app/overrides/spree/admin/shared/_configuration_menu/add_caching.html.haml.deface b/app/overrides/spree/admin/shared/_configuration_menu/add_caching.html.haml.deface new file mode 100644 index 0000000000..1eb416e72a --- /dev/null +++ b/app/overrides/spree/admin/shared/_configuration_menu/add_caching.html.haml.deface @@ -0,0 +1,4 @@ +/ insert_bottom "[data-hook='admin_configurations_sidebar_menu']" + +%li + = link_to 'Caching', main_app.admin_cache_settings_path diff --git a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface index 190cff1a17..a2f9ed766a 100644 --- a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface @@ -1,6 +1,9 @@ / replace "div[data-hook='admin_shipping_method_form_fields']" -.alpha.eleven.columns{"data-hook" => "admin_shipping_method_form_fields"} +=admin_inject_shipping_method +.alpha.eleven.columns{ "data-hook" => "admin_shipping_method_form_fields", + "ng-app" => "admin.shippingMethods", + "ng-controller" => "shippingMethodCtrl" } .row .alpha.three.columns = f.label :name, t(:name) @@ -46,6 +49,13 @@   = f.label :pick_up, t(:pick_up) + .row + .alpha.three.columns + = f.label :tags, t(:tags) + .omega.eight.columns + = f.hidden_field :tag_list, "ng-value" => "shippingMethod.tag_list" + %tags-with-translation#something{ object: "shippingMethod" } + .row .alpha.eleven.columns - = render :partial => 'spree/admin/shared/calculator_fields', :locals => { :f => f } \ No newline at end of file + = render :partial => 'spree/admin/shared/calculator_fields', :locals => { :f => f } diff --git a/app/serializers/api/admin/calculator/flat_percent_item_total_serializer.rb b/app/serializers/api/admin/calculator/flat_percent_item_total_serializer.rb new file mode 100644 index 0000000000..7662edb3f6 --- /dev/null +++ b/app/serializers/api/admin/calculator/flat_percent_item_total_serializer.rb @@ -0,0 +1,7 @@ +class Api::Admin::Calculator::FlatPercentItemTotalSerializer < ActiveModel::Serializer + attributes :id, :preferred_flat_percent + + def preferred_flat_percent + object.preferred_flat_percent.to_i + end +end diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 37a96b402f..8f70da1a0d 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -3,8 +3,25 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer 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 + attributes :owner, :users, :tag_groups has_one :owner, serializer: Api::Admin::UserSerializer has_many :users, serializer: Api::Admin::UserSerializer + + def tag_groups + tag_groups = [] + object.tag_rules.each do |tag_rule| + tag_group = find_match(tag_groups, tag_rule.preferred_customer_tags.split(",").map{ |t| { text: t } }) + tag_groups << tag_group if tag_group[:rules].empty? + tag_group[:rules] << Api::Admin::TagRuleSerializer.new(tag_rule).serializable_hash + end + tag_groups + end + + def find_match(tag_groups, tags) + tag_groups.each do |tag_group| + return tag_group if tag_group[:tags].length == tags.length && (tag_group[:tags] & tags) == tag_group[:tags] + end + return { tags: tags, rules: [] } + end end diff --git a/app/serializers/api/admin/shipping_method_serializer.rb b/app/serializers/api/admin/shipping_method_serializer.rb new file mode 100644 index 0000000000..9fbb864d09 --- /dev/null +++ b/app/serializers/api/admin/shipping_method_serializer.rb @@ -0,0 +1,7 @@ +class Api::Admin::ShippingMethodSerializer < ActiveModel::Serializer + attributes :id, :name, :tags + + def tags + object.tag_list.map{ |t| { text: t } } + end +end diff --git a/app/serializers/api/admin/tag_rule_serializer.rb b/app/serializers/api/admin/tag_rule_serializer.rb new file mode 100644 index 0000000000..ac333c23c6 --- /dev/null +++ b/app/serializers/api/admin/tag_rule_serializer.rb @@ -0,0 +1,27 @@ +class Api::Admin::TagRuleSerializer < ActiveModel::Serializer + def serializable_hash + rule_specific_serializer.serializable_hash + end + + def rule_specific_serializer + "Api::Admin::#{object.class.to_s}Serializer".constantize.new(object) + end +end + +module Api::Admin::TagRule + class BaseSerializer < ActiveModel::Serializer + attributes :id, :enterprise_id, :type, :preferred_customer_tags + end + + class DiscountOrderSerializer < BaseSerializer + has_one :calculator, serializer: Api::Admin::Calculator::FlatPercentItemTotalSerializer + end + + class FilterShippingMethodsSerializer < BaseSerializer + attributes :preferred_matched_shipping_methods_visibility, :shipping_method_tags + + def shipping_method_tags + object.preferred_shipping_method_tags.split(",") + end + end +end diff --git a/app/serializers/api/order_serializer.rb b/app/serializers/api/order_serializer.rb new file mode 100644 index 0000000000..acbd0fdd01 --- /dev/null +++ b/app/serializers/api/order_serializer.rb @@ -0,0 +1,31 @@ +module Api + class OrderSerializer < ActiveModel::Serializer + attributes :number, :completed_at, :total, :state, :shipment_state, :payment_state, :outstanding_balance, :payments, :path + + has_many :payments, serializer: Api::PaymentSerializer + + def completed_at + object.completed_at.blank? ? "" : I18n.l(object.completed_at, format: :long) + end + + def total + object.total.to_money.to_s + end + + def shipment_state + object.shipment_state ? object.shipment_state : nil + end + + def payment_state + object.payment_state ? object.payment_state : nil + end + + def state + object.state ? object.state : nil + end + + def path + Spree::Core::Engine.routes_url_helpers.order_url(object.number, only_path: true) + end + end +end diff --git a/app/serializers/api/orders_by_distributor_serializer.rb b/app/serializers/api/orders_by_distributor_serializer.rb new file mode 100644 index 0000000000..b920272df2 --- /dev/null +++ b/app/serializers/api/orders_by_distributor_serializer.rb @@ -0,0 +1,18 @@ +module Api + class OrdersByDistributorSerializer < ActiveModel::Serializer + attributes :name, :id, :hash, :balance, :logo, :distributed_orders + has_many :distributed_orders, serializer: Api::OrderSerializer + + def balance + object.distributed_orders.map(&:outstanding_balance).reduce(:+).to_money.to_s + end + + def hash + object.to_param + end + + def logo + object.logo(:small) if object.logo? + end + end +end diff --git a/app/serializers/api/payment_serializer.rb b/app/serializers/api/payment_serializer.rb new file mode 100644 index 0000000000..d48f75a07f --- /dev/null +++ b/app/serializers/api/payment_serializer.rb @@ -0,0 +1,16 @@ +module Api + class PaymentSerializer < ActiveModel::Serializer + attributes :amount, :updated_at, :payment_method + def payment_method + object.payment_method.name + end + + def amount + object.amount.to_money.to_s + end + + def updated_at + I18n.l(object.updated_at, format: :long) + end + end +end diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index 03a66afe89..aa353173dc 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -36,7 +36,7 @@ class Api::CachedProductSerializer < ActiveModel::Serializer #delegate :cache_key, to: :object include ActionView::Helpers::SanitizeHelper - attributes :id, :name, :permalink, :count_on_hand + attributes :id, :name, :permalink attributes :on_demand, :group_buy, :notes, :description attributes :properties_with_values diff --git a/app/views/admin/accounts_and_billing_settings/edit.html.haml b/app/views/admin/accounts_and_billing_settings/edit.html.haml index 71fac1fd28..72ce7920a9 100644 --- a/app/views/admin/accounts_and_billing_settings/edit.html.haml +++ b/app/views/admin/accounts_and_billing_settings/edit.html.haml @@ -1,14 +1,15 @@ = render :partial => 'spree/admin/shared/configuration_menu' - content_for :page_title do - = t(:accounts_and_billing_settings) + = t(:accounts_and_billing) = render 'spree/shared/error_messages', target: @settings -# - month_options = (0...12).map { |i| Time.zone.now.beginning_of_month - i.months }.map{ |t| [t.strftime("%b %Y"), t.strftime("%b %Y %z")]} %fieldset.no-border-bottom - %legend Settings + %legend + =t :admin_settings = form_for @settings, as: :settings, url: main_app.admin_accounts_and_billing_settings_path, :method => :put do |f| .row{ ng: { app: 'admin.accounts_and_billing_settings' } } .twelve.columns.alpha.omega @@ -23,26 +24,32 @@ .row .six.columns.alpha %fieldset.no-border-bottom - %legend Update Invoices + %legend + =t :update_invoice = f.check_box :auto_update_invoices - = f.label :auto_update_invoices, "Auto-update invoices nightly at 1:00am" + = f.label :auto_update_invoices, + t(:auto_update_invoices) .six.columns.omega %fieldset.no-border-bottom - %legend Finalise Invoices + %legend + =t :finalise_invoice = f.check_box :auto_finalize_invoices - = f.label :auto_finalize_invoices, "Auto-finalise invoices monthly on the 2nd at 1:30am" + = f.label :auto_finalize_invoices, + t(:auto_finalise_invoices) .row .twelve.columns.alpha.omega.form-buttons{"data-hook" => "buttons"} = button t(:update), 'icon-refresh', value: "update" %fieldset.no-border-bottom - %legend Manually Run Tasks + %legend + =t :manually_run_task .row .six.columns.alpha.step.text-center .form-buttons{"data-hook" => "buttons"} - =link_to_with_icon "icon-undo", "Update User Invoices", + =link_to_with_icon "icon-undo", + t(:update_user_invoices) , main_app.start_job_admin_accounts_and_billing_settings_path(job: { name: "update_account_invoices" }), class: "button fullwidth" @@ -51,22 +58,22 @@ - if @update_account_invoices_job %p.text-center - if @update_account_invoices_job.run_at < Time.zone.now - %strong In Progress + =t :in_progress %br - Started at: + =t :started_at - else - %strong Queued + %strong + =t :queued %br - Scheduled for: + =t :Scheduled_for = @update_account_invoices_job.run_at - else %p.explanation - Use this button to immediately update invoices for the month to date for each enterprise user in the system. This task can be set up to run automatically every night. - + =t :update_user_invoice_explained .six.columns.omega.step.text-center .form-buttons{"data-hook" => "buttons"} - =link_to_with_icon "icon-ok-sign", "Finalise User Invoices", + =link_to_with_icon "icon-ok-sign", t(:finalise_user_invoices ), main_app.start_job_admin_accounts_and_billing_settings_path(job: { name: "finalize_account_invoices" }), class: "button fullwidth" @@ -75,14 +82,16 @@ - if @finalize_account_invoices_job %p.text-center - if @finalize_account_invoices_job.run_at < Time.zone.now - %strong In Progress + %strong + =t :in_progress %br - Started at: + =t :started_at - else - %strong Queued + %strong + =t :queued %br - Scheduled for: + =t :scheduled_for = @finalize_account_invoices_job.run_at - else %p.explanation - Use this button to finalize all invoices in the system for the previous calendar month. This task can be set up to run automatically once a month. + =t :finalise_user_invoice_explained diff --git a/app/views/admin/cache_settings/show.html.haml b/app/views/admin/cache_settings/show.html.haml new file mode 100644 index 0000000000..79b3e5acaf --- /dev/null +++ b/app/views/admin/cache_settings/show.html.haml @@ -0,0 +1,18 @@ +- content_for :page_title do + = t(:cache_settings) + +%table.index + %thead + %tr + %th Distributor + %th Order Cycle + %th Status + %th Diff + %tbody + - @results.each do |result| + %tr + %td= result[:distributor].name + %td= result[:order_cycle].name + %td= result[:status] ? 'OK' : 'Error' + %td + %pre= result[:diff].to_s(:text) diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index 5647821bf5..e33b871df4 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -1,33 +1,35 @@ - content_for :page_title do - %h1.page-title Customers + %h1.page-title + =t :customers = admin_inject_shops %div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } } - .row{ ng: { hide: "loaded() && filteredCustomers.length > 0" } } + .row{ ng: { hide: "loaded && customers.length > 0" } } .five.columns.alpha - %h3 Please select a Hub: + %h3 + =t :please_select_hub .four.columns %select.select2.fullwidth#shop_id{ 'ng-model' => 'shop.id', name: 'shop_id', 'ng-options' => 'shop.id as shop.name for shop in shops' } .seven.columns.omega   - .row{ 'ng-hide' => '!loaded() || filteredCustomers.length == 0' } + .row{ 'ng-hide' => '!loaded || customers.length == 0' } .controls.sixteen.columns.alpha.omega .five.columns.alpha %input.fullwidth{ :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } - .five.columns   - -# =render 'admin/shared/bulk_actions_dropdown' - .three.columns   + .eight.columns   = render 'admin/shared/columns_dropdown' - .row{ 'ng-if' => 'shop && !loaded()' } + .row{ 'ng-if' => 'shop.id && !loaded' } .sixteen.columns.alpha#loading %img.spinner{ src: "/assets/spinning-circles.svg" } - %h1 LOADING CUSTOMERS - .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredCustomers.length == 0'} - %h1#no_results No customers found. + %h1 + =t :loading_customers + .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded && filteredCustomers.length == 0'} + %h1#no_results + =t :no_customers_found - .row{ ng: { show: "loaded() && filteredCustomers.length > 0" } } + .row{ ng: { show: "loaded && filteredCustomers.length > 0" } } %form{ name: "customers" } %table.index#customers %col.email{ width: "20%"} @@ -58,3 +60,11 @@ %td.actions %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' } + + %form{ng: {show: "loaded", submit: 'add(newCustomerEmail)'}} + %h2= t '.add_new_customer' + .row + .five.columns.alpha + %input.fullwidth{type: "text", placeholder: t('.customer_placeholder'), ng: {model: 'newCustomerEmail'}} + .eleven.columns.omega + %input{type: "submit", value: t('.add_customer')} diff --git a/app/views/admin/enterprise_fees/index.html.haml b/app/views/admin/enterprise_fees/index.html.haml index 8a1e972a33..ec62fa5d36 100644 --- a/app/views/admin/enterprise_fees/index.html.haml +++ b/app/views/admin/enterprise_fees/index.html.haml @@ -1,5 +1,5 @@ = content_for :page_title do - Enterprise Fees + =t :Enterprise_Fees = 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 @@ -11,12 +11,18 @@ %table.index#listing_enterprise_fees %thead %tr - %th Enterprise - %th Fee Type - %th Name - %th Tax Category - %th Calculator - %th Calculator values + %th + =t'Enterprise' + %th + =t'fee_type' + %th + =t'name' + %th + =t'tax_category' + %th + =t'calculator' + %th + =t'calculator_values' %th.actions %tbody = enterprise_fee_set_form.ng_fields_for :collection do |f| @@ -37,4 +43,4 @@ %td{'ng-bind-html-unsafe-compiled' => 'enterprise_fee.calculator_settings'} %td.actions{'spree-delete-resource' => "1"} - = enterprise_fee_set_form.submit 'Update' + = enterprise_fee_set_form.submit t(:update) diff --git a/app/views/admin/enterprise_groups/_form_about.html.haml b/app/views/admin/enterprise_groups/_form_about.html.haml index a29fde22cd..1e97697a4e 100644 --- a/app/views/admin/enterprise_groups/_form_about.html.haml +++ b/app/views/admin/enterprise_groups/_form_about.html.haml @@ -1,5 +1,6 @@ %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } - %legend About + %legend + = t 'admin_entreprise_groups_about' = f.field_container :long_description do %text-angular{'id' => 'enterprise_group_long_description', 'name' => 'enterprise_group[long_description]', 'class' => 'text-angular', 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]"} diff --git a/app/views/admin/enterprise_groups/_form_address.html.haml b/app/views/admin/enterprise_groups/_form_address.html.haml index 12bb7ef315..fa7b1e7e75 100644 --- a/app/views/admin/enterprise_groups/_form_address.html.haml +++ b/app/views/admin/enterprise_groups/_form_address.html.haml @@ -1,11 +1,12 @@ = f.fields_for :address do |af| %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } - %legend Contact + %legend + = t 'admin_entreprise_groups_contact' .row .alpha.three.columns = af.label :phone .omega.eight.columns - = af.text_field :phone, { placeholder: "eg. 98 7654 3210"} + = af.text_field :phone, { placeholder: t(:admin_entreprise_groups_contact_phone_placeholder)} .row .alpha.three.columns = f.label :email @@ -15,7 +16,7 @@ .three.columns.alpha = af.label :address1 .eight.columns.omega - = af.text_field :address1, { placeholder: "eg. 123 High Street"} + = af.text_field :address1, { placeholder: t(:admin_entreprise_groups_contact_address1_placeholder)} .row .alpha.three.columns = af.label :address2 @@ -23,18 +24,17 @@ = af.text_field :address2 .row .three.columns.alpha - = af.label :city, 'Suburb' + = af.label :city, t(:admin_entreprise_groups_contact_city) \/ - = af.label :zipcode, 'Postcode' + = af.label :zipcode, t(:admin_entreprise_groups_contact_zipcode) .four.columns - = af.text_field :city, { placeholder: "eg. Northcote"} + = af.text_field :city, { placeholder: t(:admin_entreprise_groups_contact_city_placeholder)} .four.columns.omega - = af.text_field :zipcode, { placeholder: "eg. 3070"} + = af.text_field :zipcode, { placeholder: t(:admin_entreprise_groups_contact_zipcode_placeholder)} .row .three.columns.alpha - = af.label :state_id, 'State' - \/ - = af.label :country_id, 'Country' + = af.label :state_id, t(:admin_entreprise_groups_contact_state_id) + = af.label :country_id, t(:admin_entreprise_groups_contact_country_id) .four.columns = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" .four.columns.omega diff --git a/app/views/admin/enterprise_groups/_form_images.html.haml b/app/views/admin/enterprise_groups/_form_images.html.haml index 1bfffa86d2..24bfad3bf0 100644 --- a/app/views/admin/enterprise_groups/_form_images.html.haml +++ b/app/views/admin/enterprise_groups/_form_images.html.haml @@ -1,18 +1,19 @@ %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Images'" } } - %legend Images + %legend + = t 'admin_entreprise_groups_images' .row .alpha.three.columns - = 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? + = f.label :logo, 'ofn-with-tip' => t('admin_entreprise_groups_data_powertip_logo') + %div{'ofn-with-tip' => t('admin_entreprise_groups_data_powertip_logo')} + %a= t 'admin.whats_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, '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? + = f.label :promo_image, 'ofn-with-tip' => t(:admin_entreprise_groups_data_powertip_promo_image) + %div{'ofn-with-tip' => t('admin_entreprise_groups_data_powertip_promo_image')} + %a= t 'admin.whats_this' .omega.eight.columns = image_tag @object.promo_image.url if @object.promo_image.present? = f.file_field :promo_image diff --git a/app/views/admin/enterprise_groups/_form_primary_details.html.haml b/app/views/admin/enterprise_groups/_form_primary_details.html.haml index d57333626f..56771c458c 100644 --- a/app/views/admin/enterprise_groups/_form_primary_details.html.haml +++ b/app/views/admin/enterprise_groups/_form_primary_details.html.haml @@ -1,5 +1,6 @@ %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Primary Details'" } } - %legend Primary Details + %legend + = t "admin_entreprise_groups_primary_details" = f.field_container :name do = f.label :name %br/ @@ -11,12 +12,12 @@ = f.text_field :description = f.field_container :on_front_page do - = f.label :on_front_page, 'On front page?' + = f.label :on_front_page, t(:admin_entreprise_groups_on_front_page) %br/ = f.check_box :on_front_page = f.field_container :enterprise_ids do - = f.label :enterprise_ids, 'Enterprises' + = f.label :enterprise_ids, t(:admin_entreprise_groups_entreprise) %br/ = f.collection_select :enterprise_ids, @enterprises, :id, :name, {}, {class: "select2 fullwidth", multiple: true} diff --git a/app/views/admin/enterprise_groups/_form_users.html.haml b/app/views/admin/enterprise_groups/_form_users.html.haml index c4f57da48a..6bd6eaa4a3 100644 --- a/app/views/admin/enterprise_groups/_form_users.html.haml +++ b/app/views/admin/enterprise_groups/_form_users.html.haml @@ -1,10 +1,12 @@ %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Users'" } } - %legend Users + %legend + = t(:users) .row .three.columns.alpha - =f.label :owner_id, 'Owner' - %div{'ofn-with-tip' => "The primary user responsible for this group."} - %a What's this? + =f.label :owner_id, t(:admin_entreprise_groups_owner) + .with-tip{'data-powertip' => t(:admin_entreprise_groups_data_powertip)} + %a + = t 'admin.whats_this' .eight.columns.omega - if spree_current_user.admin? = f.hidden_field :owner_id, diff --git a/app/views/admin/enterprise_groups/_form_web.html.haml b/app/views/admin/enterprise_groups/_form_web.html.haml index 42638d94c6..5d982f1bbb 100644 --- a/app/views/admin/enterprise_groups/_form_web.html.haml +++ b/app/views/admin/enterprise_groups/_form_web.html.haml @@ -1,10 +1,11 @@ %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Web'" } } - %legend Web Resources + %legend + = t 'admin_entreprise_groups_web' .row .alpha.three.columns = f.label :website .omega.eight.columns - = f.text_field :website, { placeholder: "eg. www.truffles.com"} + = f.text_field :website, { placeholder: t(:admin_entreprise_groups_web_website_placeholder)} .row .alpha.three.columns = f.label :facebook, 'Facebook' @@ -24,4 +25,4 @@ .alpha.three.columns = f.label :twitter .omega.eight.columns - = f.text_field :twitter, { placeholder: "eg. @the_prof" } + = f.text_field :twitter, { placeholder: t(:admin_entreprise_groups_web_twitter) } diff --git a/app/views/admin/enterprise_groups/index.html.haml b/app/views/admin/enterprise_groups/index.html.haml index e78dadc72c..747eff0b94 100644 --- a/app/views/admin/enterprise_groups/index.html.haml +++ b/app/views/admin/enterprise_groups/index.html.haml @@ -1,5 +1,5 @@ = content_for :page_title do - Enterprise Groups + = t 'admin_entreprise_groups' - if admin_user? = content_for :page_actions do @@ -8,11 +8,15 @@ %table.index#listing_enterprise_groups %thead %tr - %th Name + %th + = t 'admin_entreprise_groups_name' - if spree_current_user.admin? - %th Owner - %th On front page? - %th Enterprises + %th + = t 'admin_entreprise_groups_owner' + %th + = t 'admin_entreprise_groups_on_front_page' + %th + = t 'admin_entreprise_groups_entreprise' %th.actions %tbody diff --git a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml index 996bc487d8..05e6505868 100644 --- a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml +++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml @@ -1,6 +1,7 @@ %tr{"ng-repeat" => "enterprise_relationship in EnterpriseRelationships.enterprise_relationships | keywords:query"} %td {{ enterprise_relationship.parent_name }} - %td permits + %td + = t 'admin_entreprise_relationships_permits' %td {{ enterprise_relationship.child_name }} %td %ul diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index b130faeb1a..4d68d61e1b 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -3,17 +3,17 @@ %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 + = t 'admin_entreprise_relationships_permits' %td %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()"}} - Everything + = t 'admin_entreprise_relationships_everything' %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} %label %input{type: "checkbox", "ng-model" => "permissions[permission]"} to {{ EnterpriseRelationships.permission_presentation(permission) }} %td.actions - %input{type: "button", value: "Create", "ng-click" => "create()"} + %input{type: "button", value: t(:admin_entreprise_relationships_button_create), "ng-click" => "create()"} .errors {{ EnterpriseRelationships.create_errors }} diff --git a/app/views/admin/enterprise_relationships/_search_input.html.haml b/app/views/admin/enterprise_relationships/_search_input.html.haml index b8bcbc62c6..350089dc4f 100644 --- a/app/views/admin/enterprise_relationships/_search_input.html.haml +++ b/app/views/admin/enterprise_relationships/_search_input.html.haml @@ -1,4 +1,4 @@ -%input.search{"ng-model" => "query", "placeholder" => "Search"} +%input.search{"ng-model" => "query", "placeholder" => t(:admin_entreprise_relationships_seach_placeholder)} %label{ng: {repeat: "permission in EnterpriseRelationships.all_permissions"}} %input{type: "checkbox", ng: {click: "$parent.query = toggleKeyword($parent.query, permission)"}} diff --git a/app/views/admin/enterprise_relationships/index.html.haml b/app/views/admin/enterprise_relationships/index.html.haml index 163200a0fc..bdff595f09 100644 --- a/app/views/admin/enterprise_relationships/index.html.haml +++ b/app/views/admin/enterprise_relationships/index.html.haml @@ -1,5 +1,5 @@ - content_for :page_title do - Enterprise Relationships + = t 'admin_entreprise_relationships' = render 'admin/shared/enterprises_sub_menu' diff --git a/app/views/admin/enterprise_roles/index.html.haml b/app/views/admin/enterprise_roles/index.html.haml index a6e0985a99..45d8115597 100644 --- a/app/views/admin/enterprise_roles/index.html.haml +++ b/app/views/admin/enterprise_roles/index.html.haml @@ -1,5 +1,5 @@ - content_for :page_title do - Roles + =t :roles = render 'admin/shared/users_sub_menu' diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 79faeea229..e363078733 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -54,3 +54,7 @@ %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Shop Preferences'" } } %legend Shop Preferences = render 'admin/enterprises/form/shop_preferences', f: f + +%fieldset.alpha.no-border-bottom{ ng: { if: "menu.selected.name=='Tag Rules'" } } + %legend Tag Rules + = render 'admin/enterprises/form/tag_rules', f: f diff --git a/app/views/admin/enterprises/edit.html.haml b/app/views/admin/enterprises/edit.html.haml index 19ae0f3b44..5d3a623f40 100644 --- a/app/views/admin/enterprises/edit.html.haml +++ b/app/views/admin/enterprises/edit.html.haml @@ -7,6 +7,7 @@ - content_for :page_actions do %li= button_link_to "Back to enterprises list", main_app.admin_enterprises_path, icon: 'icon-arrow-left' + = render 'admin/enterprises/form_data' = render 'admin/enterprises/ng_form', action: 'edit' diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml index 437a61e866..ccf59c8a12 100644 --- a/app/views/admin/enterprises/form/_primary_details.html.haml +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -22,7 +22,6 @@ %a What's this? .five.columns.omega = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' -   = f.label :is_primary_producer, 'Producer' - if spree_current_user.admin? .row @@ -33,15 +32,12 @@ %a What's this? .two.columns = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' -   = f.label :sells, "None", value: "none" .two.columns = f.radio_button :sells, "own", 'ng-model' => 'Enterprise.sells' -   = f.label :sells, "Own", value: "own" .four.columns.omega = f.radio_button :sells, "any", 'ng-model' => 'Enterprise.sells' -   = f.label :sells, "Any", value: "any" .row .three.columns.alpha @@ -50,12 +46,21 @@ %a What's this? .two.columns = f.radio_button :visible, true -   = f.label :visible, "Visible", :value => "true" .five.columns.omega = f.radio_button :visible, false -   = f.label :visible, "Not Visible", :value => "false" +.row + .three.columns.alpha + %label= t '.shopfront_requires_login' + %div{'ofn-with-tip' => t('.shopfront_requires_login_tip')} + %a= t 'admin.whats_this' + .two.columns + = f.radio_button :require_login, false + = f.label :require_login, t('.shopfront_requires_login_false'), value: :false + .five.columns.omega + = f.radio_button :require_login, true + = f.label :require_login, t('.shopfront_requires_login_true'), value: :true .permalink{ ng: { controller: "permalinkCtrl" } } .row{ ng: { show: "Enterprise.sells == 'own' || Enterprise.sells == 'any'" } } .three.columns.alpha diff --git a/app/views/admin/enterprises/form/_shipping_methods.html.haml b/app/views/admin/enterprises/form/_shipping_methods.html.haml index 445b026c38..5ccc22a720 100644 --- a/app/views/admin/enterprises/form/_shipping_methods.html.haml +++ b/app/views/admin/enterprises/form/_shipping_methods.html.haml @@ -7,7 +7,7 @@ %th %tbody - @shipping_methods.each do |shipping_method| - %tr{ ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } + %tr{ ng: { controller: 'shippingMethodsCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } %td= shipping_method.name %td= f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil %td= link_to "Edit", edit_admin_shipping_method_path(shipping_method) diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml new file mode 100644 index 0000000000..1a50e4f353 --- /dev/null +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -0,0 +1,33 @@ +.row{ ng: { controller: "TagRulesCtrl" } } + .eleven.columns.alpha.omega + .eleven.columns.alpha.omega + .no_tags{ ng: { show: "tagGroups.length == 0" } } + No tags apply to this enterprise yet + .customer_tag{ ng: { repeat: "tagGroup in tagGroups" }, bindonce: true } + .header + %table + %colgroup + %col{width: '35%'} + %col{width: '65%'} + %tr + %td + %h5 + For customers tagged: + %td + %tags-input{ ng: { model: 'tagGroup.tags'}, + min: { tags: "1" }, + on: { tag: { added: "updateTagsRulesFor(tagGroup)", removed: "updateTagsRulesFor(tagGroup)" } } } + + .no_rules{ ng: { show: "tagGroup.rules.length == 0" } } + No rules apply to this tag yet + %table + %tr.tag_rule{ id: "tr_{{rule.id}}", ng: { repeat: "rule in tagGroup.rules" } } + %td + %discount-order{ bo: { if: "rule.type == 'TagRule::DiscountOrder'" } } + %filter-shipping-methods{ bo: { if: "rule.type == 'TagRule::FilterShippingMethods'" } } + %td.actions + %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } + .add_rule.text-center + %input.button.icon-plus{ type: 'button', value: "+ Add A New Rule", "new-tag-rule-dialog" => true } + .add_tage + %input.button.red.icon-plus{ type: 'button', value: "+ Add A New Tag", ng: { click: 'addNewTag()' } } diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index d2b7605289..f3b0bdb721 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -22,7 +22,8 @@ #advanced_settings{ hidden: true } = render partial: "/admin/order_cycles/advanced_settings" -%h1 Edit Order Cycle +%h1 + = t :edit_order_cycle - ng_controller = order_cycles_simple_form ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index 8f062ff7db..824aa66ee8 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -1,15 +1,15 @@ = content_for :page_title do - Order Cycles + = t :admin_order_cycles = content_for :page_actions do %li#new_order_cycle_link - = button_link_to "New Order Cycle", main_app.new_admin_order_cycle_path, :icon => 'icon-plus', :id => 'admin_new_order_cycle_link' + = button_link_to t(:new_order_cycle), main_app.new_admin_order_cycle_path, :icon => 'icon-plus', :id => 'admin_new_order_cycle_link' - if @show_more %li - = button_link_to "Show less", main_app.admin_order_cycles_path + = button_link_to t(:label_less), main_app.admin_order_cycles_path - else %li - = button_link_to "Show more", main_app.admin_order_cycles_path(params: { show_more: true }) + = button_link_to t(:label_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, html: {"ng-app" => "admin.orderCycles"} do |f| %table.index#listing_order_cycles @@ -28,14 +28,21 @@ %thead %tr - %th Name - %th Open - %th Close + %th + =t :name + %th + =t :open + %th + =t :close - unless order_cycles_simple_index - %th Supplier - %th Coordinator - %th Distributors - %th Products + %th + =t :supplier + %th + =t :coordinator + %th + =t :distributors + %th + =t :products %th.actions %th.actions %th.actions @@ -44,4 +51,4 @@ = f.fields_for :collection do |order_cycle_form| = render 'admin/order_cycles/row', order_cycle_form: order_cycle_form - = f.submit 'Update' + = f.submit t :update diff --git a/app/views/admin/order_cycles/new.html.haml b/app/views/admin/order_cycles/new.html.haml index 460f2e08ca..a257bf3c0b 100644 --- a/app/views/admin/order_cycles/new.html.haml +++ b/app/views/admin/order_cycles/new.html.haml @@ -1,4 +1,5 @@ -%h1 New Order Cycle +%h1 + =t'new_order_cycle' - ng_controller = order_cycles_simple_form ? 'AdminSimpleCreateOrderCycleCtrl' : 'AdminCreateOrderCycleCtrl' = admin_inject_order_cycle_instance diff --git a/app/views/admin/order_cycles/set_coordinator.html.haml b/app/views/admin/order_cycles/set_coordinator.html.haml index f2663cbfd2..db9bbb926c 100644 --- a/app/views/admin/order_cycles/set_coordinator.html.haml +++ b/app/views/admin/order_cycles/set_coordinator.html.haml @@ -1,4 +1,5 @@ -%h4.text-center Select a coordinator for your order cycle +%h4.text-center + =t'select_a_coordinator_for_your_order_cycle' %br @@ -9,7 +10,7 @@ .ten.columns = select_tag :coordinator_id, options_for_select(permitted_coordinating_enterprise_options_for(@order_cycle)), { 'required' => true, class: 'select2 fullwidth'} .two.columns.alpha - = f.submit "Continue >" + = f.submit "#{t(:continue)} >" .two.columns.omega   diff --git a/app/views/admin/producer_properties/index.html.haml b/app/views/admin/producer_properties/index.html.haml index d8eac9e816..d898860cb1 100644 --- a/app/views/admin/producer_properties/index.html.haml +++ b/app/views/admin/producer_properties/index.html.haml @@ -6,7 +6,7 @@ - content_for :page_actions do %ul.tollbar.inline-menu %li - = link_to_add_fields 'Add Producer Property', 'tbody#producer_properties', class: 'icon-plus button' + = link_to_add_fields t(:add_producer_property), 'tbody#producer_properties', class: 'icon-plus button' = render 'spree/shared/error_messages', target: @enterprise diff --git a/app/views/admin/variant_overrides/_filters.html.haml b/app/views/admin/variant_overrides/_filters.html.haml index 7330c26799..c9f3a27b32 100644 --- a/app/views/admin/variant_overrides/_filters.html.haml +++ b/app/views/admin/variant_overrides/_filters.html.haml @@ -1,4 +1,4 @@ -.filters.sixteen.columns.alpha +.filters.sixteen.columns.alpha.omega .filter.four.columns.alpha %label{ :for => 'query', ng: {class: '{disabled: !hub_id}'} }=t('admin.quick_search') %br diff --git a/app/views/admin/variant_overrides/_show_more.html.haml b/app/views/admin/variant_overrides/_show_more.html.haml index ad943c853e..8d60593ddc 100644 --- a/app/views/admin/variant_overrides/_show_more.html.haml +++ b/app/views/admin/variant_overrides/_show_more.html.haml @@ -1,4 +1,4 @@ -.sixteen.columns.alpha.omega.text-center{ ng: {show: 'productLimit < filteredProducts.length'}} +.text-center %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/checkout/_authentication.html.haml b/app/views/checkout/_authentication.html.haml index 2120ba6549..1c3b06e63b 100644 --- a/app/views/checkout/_authentication.html.haml +++ b/app/views/checkout/_authentication.html.haml @@ -1,11 +1,11 @@ %section{"ng-show" => "!enabled"} .row - .small-12.columns.text-center{"ng-controller" => "AuthenticationCtrl"} + .small-12.columns.text-center %h3.pad-top = t :checkout_headline .row.pad-top - .small-5.columns.text-center{"ng-controller" => "AuthenticationCtrl"} - %button.primary.expand{"ng-click" => "open()"} + .small-5.columns.text-center + %button.primary.expand{"auth" => "login"} = t :label_login .small-2.columns.text-center %p.pad-top= "-#{t :action_or}-" diff --git a/app/views/enterprises/shop.html.haml b/app/views/enterprises/shop.html.haml index f2063494b4..d635f68164 100644 --- a/app/views/enterprises/shop.html.haml +++ b/app/views/enterprises/shop.html.haml @@ -22,6 +22,7 @@ %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", "ofn-change-order-cycle" => true, + "disabled" => require_customer?, "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", "popover-placement" => "left", "popover" => t(:enterprises_choose), "popover-trigger" => "openTrigger"} @@ -31,7 +32,7 @@ = render partial: 'shop/messages' - .row - = render partial: "shop/products/form" + - unless require_customer? + .row= render partial: "shop/products/form" = render partial: "shared/footer" diff --git a/app/views/home/_hubs.html.haml b/app/views/home/_hubs.html.haml index fea1fc0a7d..e899270efc 100644 --- a/app/views/home/_hubs.html.haml +++ b/app/views/home/_hubs.html.haml @@ -27,3 +27,9 @@ .show-distance-matches{"ng-show" => "nameMatchesFiltered.length > 0 && !distanceMatchesShown"} %a{href: "", "ng-click" => "showDistanceMatches()"} = t :hubs_distance_filter, location: "{{ nameMatchesFiltered[0].name }}" + .more-controls + %a.button{href: "", ng: {click: "showClosedShops()", show: "filterExpression.active"}} + = t '.show_closed_shops' + %a.button{href: "", ng: {click: "hideClosedShops()", show: "!filterExpression.active"}} + = t '.hide_closed_shops' + %a.button{href: main_app.map_path}= t '.show_on_map' diff --git a/app/views/home/_hubs_table.html.haml b/app/views/home/_hubs_table.html.haml index 8842079f56..edf9eb5ec8 100644 --- a/app/views/home/_hubs_table.html.haml +++ b/app/views/home/_hubs_table.html.haml @@ -1,5 +1,5 @@ .active_table - %hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | visible | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+distance', '+orders_close_at'])", + %hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | filter:filterExpression | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+distance', '+orders_close_at'])", "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", "ng-controller" => "HubNodeCtrl", id: "{{hub.hash}}"} diff --git a/app/views/producers/_skinny.html.haml b/app/views/producers/_skinny.html.haml index 11860d35d6..cf066be05a 100644 --- a/app/views/producers/_skinny.html.haml +++ b/app/views/producers/_skinny.html.haml @@ -1,20 +1,20 @@ .row.active_table_row{"ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}"} .columns.small-12.medium-4.large-4.skinny-head %span{"bo-if" => "producer.is_distributor" } - %a.is_distributor{"bo-href" => "producer.path" } + %a.is_distributor{"bo-href" => "producer.path" } %i{bo: {class: "producer.producer_icon_font"}} - %span.margin-top + %span.margin-top %strong{"bo-text" => "producer.name"} %span.producer-name{"bo-if" => "!producer.is_distributor" } %i{bo: {class: "producer.producer_icon_font"}} - %span.margin-top + %span.margin-top %strong{"bo-text" => "producer.name"} - + .columns.small-6.medium-3.large-3 %span.margin-top{"bo-text" => "producer.address.city"} .columns.small-4.medium-3.large-4 %span.margin-top{"bo-bind" => "producer.address.state_name | uppercase"} .columns.small-2.medium-2.large-1.text-right - %span.margin-top + %span.margin-top %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index fd7f5faf4e..e3e1f44174 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -7,13 +7,7 @@ .row .small-12.medium-8.medium-offset-2.columns.text-center .alert-box - %a.big-alert{href: "http://www.openfoodnetwork.org", target: "_blank"} - %h6 - = t :alert_selling_on_ofn -   - %strong - = t :alert_start_here - %i.ofn-i_054-point-right + = render 'shared/register_call' .row .small-12.medium-4.medium-offset-2.columns.text-center %h6 diff --git a/app/views/shared/_register_call.html.haml b/app/views/shared/_register_call.html.haml new file mode 100644 index 0000000000..8c2d95ed2e --- /dev/null +++ b/app/views/shared/_register_call.html.haml @@ -0,0 +1,7 @@ +%a.alert-cta{href: registration_path, target: "_blank"} + %h6 + = t '.selling_on_ofn' +   + %strong + = t '.register' + %i.ofn-i_054-point-right diff --git a/app/views/shared/_signed_out.html.haml b/app/views/shared/_signed_out.html.haml index 1a7cbcc920..1e6efdc32a 100644 --- a/app/views/shared/_signed_out.html.haml +++ b/app/views/shared/_signed_out.html.haml @@ -1,5 +1,5 @@ -%li#login-link{"ng-controller" => "AuthenticationCtrl"} - %a{"ng-click" => "open()"} +%li#login-link + %a{"auth" => "login"} %i.ofn-i_017-locked %span = t 'label_login' diff --git a/app/views/shared/menu/_alert.html.haml b/app/views/shared/menu/_alert.html.haml index b6ee3acfb1..6512358cc3 100644 --- a/app/views/shared/menu/_alert.html.haml +++ b/app/views/shared/menu/_alert.html.haml @@ -1,10 +1,4 @@ .text-center.page-alert.fixed{ "ofn-page-alert" => true } .alert-box - %a.alert-cta{href: registration_path, target: "_blank"} - %h6 - = t 'alert_selling_on_ofn' -   - %strong - = t 'alert_start_here' - %i.ofn-i_054-point-right + = render 'shared/register_call' %a.close{ ng: { click: "close()" } } × diff --git a/app/views/shop/_messages.html.haml b/app/views/shop/_messages.html.haml index 1e56aba468..ba35354bf3 100644 --- a/app/views/shop/_messages.html.haml +++ b/app/views/shop/_messages.html.haml @@ -1,5 +1,18 @@ -- if @order_cycles and @order_cycles.empty? +- if require_customer? + .row.footer-pad + .small-12.columns + .shopfront_hidden_message + = t '.require_customer_login' + - if spree_current_user.nil? + = t '.require_login_html', + {login: ('' + t('.login') + '').html_safe, + register: ('' + t('.register') + '').html_safe} + - else + = t '.require_customer_html', + {contact: ('' + t('.contact') + '').html_safe, + enterprise: current_distributor.name} +- elsif @order_cycles and @order_cycles.empty? - if current_distributor.preferred_shopfront_closed_message.present? .row .small-12.columns diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 003c1ab5f8..43983e87ea 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -2,8 +2,10 @@ = "ng-app='admin.lineItems'" - content_for :page_title do - %h1.page-title Bulk Order Management - %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? + %h1.page-title + = t "bom_page_title" + %a{ 'ofn-with-tip' => t("bom_tip") } + = t "admin.whats_this" = render :partial => 'spree/admin/shared/order_sub_menu' @@ -11,24 +13,29 @@ %save-bar{ save: "submit()", form: "bulk_order_form" } .filters{ :class => "sixteen columns alpha" } .date_filter{ :class => "two columns alpha" } - %label{ :for => 'start_date_filter' }Start Date + %label{ :for => 'start_date_filter' } + = t "start_date" %br %input{ :class => "two columns alpha", :type => "text", :id => 'start_date_filter', 'ng-model' => 'startDate', 'datepicker' => "startDate", 'confirm-change' => "confirmRefresh()", 'ng-change' => 'refreshData()' } .date_filter{ :class => "two columns" } - %label{ :for => 'end_date_filter' }End Date + %label{ :for => 'end_date_filter' } + = t "end_date" %br %input{ :class => "two columns alpha", :type => "text", :id => 'end_date_filter', 'ng-model' => 'endDate', 'datepicker' => "endDate", 'confirm-change' => "confirmRefresh()", 'ng-change' => 'refreshData()' } .one.column   .filter_select{ :class => "three columns" } - %label{ :for => 'supplier_filter' }Producer + %label{ :for => 'supplier_filter' } + = t "producer" %br %select{ :class => "three columns alpha", :id => 'supplier_filter', 'select2-min-search' => 5, 'ng-model' => 'supplierFilter', 'ng-options' => 's.id as s.name for s in suppliers' } .filter_select{ :class => "three columns" } - %label{ :for => 'distributor_filter' }Hub + %label{ :for => 'distributor_filter' } + = t "bom_hub" %br %select{ :class => "three columns alpha", :id => 'distributor_filter', 'select2-min-search' => 5, 'ng-model' => 'distributorFilter', 'ng-options' => 'd.id as d.name for d in distributors'} .filter_select{ :class => "three columns" } - %label{ :for => 'order_cycle_filter' }Order Cycle + %label{ :for => 'order_cycle_filter' } + = t "order_cycle" %br %select{ :class => "three columns alpha", :id => 'order_cycle_filter', 'select2-min-search' => 5, 'ng-model' => 'orderCycleFilter', 'ng-options' => 'oc.id as oc.name for oc in orderCycles', 'confirm-change' => "confirmRefresh()", 'ng-change' => 'refreshData()'} .filter_clear{ :class => "two columns omega" } @@ -42,7 +49,7 @@ %div.shared_resource{ :class => "four columns alpha" } %span{ :class => 'three columns alpha' } %input{ type: 'checkbox', :id => 'shared_resource', 'ng-model' => 'sharedResource'} - Shared Resource? + = t "bom_shared" %div{ :class => "eight columns" } %h6{ :class => "eight columns alpha", 'ng-show' => 'sharedResource', style: 'text-align: center;' } {{ selectedUnitsProduct.name + ": ALL" }} %h6{ :class => "eight columns alpha", 'ng-hide' => 'sharedResource', style: 'text-align: center;' } {{ selectedUnitsVariant.full_name }} @@ -53,28 +60,33 @@ .row .one.column.alpha   .two.columns - %span.two.columns Group Buy Unit Size + %span.two.columns + = t "group_buy_unit_size" %span.two.columns {{ formattedValueWithUnitName( selectedUnitsProduct.group_buy_unit_size, selectedUnitsProduct, selectedUnitsVariant ) }} .one.column   .two.columns - %span.two.columns Total Quantity Ordered + %span.two.columns + = t "total_qtt_ordered" %span.two.columns {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }} .one.column   .two.columns - %span.two.columns Max Quantity Ordered + %span.two.columns + = t "max_qtt_ordered" %span.two.columns {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }} .one.column   .two.columns - %span.two.columns Current Fulfilled Units + %span.two.columns + = t "current_fulfilled_units" %span.two.columns {{ fulfilled(sumUnitValues()) }} .one.column   .two.columns - %span.two.columns Max Fulfilled Units + %span.two.columns + = t "max_fulfilled_units" %span.two.columns {{ fulfilled(sumMaxUnitValues()) }} .one.column.omega   %div{ :class => "eight columns alpha", 'ng-hide' => 'allFinalWeightVolumesPresent()' } %span{ :class => "eight columns alpha", style: 'color:red' } - WARNING: Some variants do not have a unit value + = t "bulk_management_warning" %hr.divider.sixteen.columns.alpha.omega @@ -87,10 +99,12 @@ %div.sixteen.columns.alpha#loading{ 'ng-if' => 'RequestMonitor.loading' } %img.spinner{ src: "/assets/spinning-circles.svg" } - %h1 LOADING ORDERS + %h1 + =t "bom_loading" %div{ :class => "sixteen columns alpha", 'ng-show' => '!RequestMonitor.loading && filteredLineItems.length == 0'} - %h1#no_results No orders found. + %h1#no_results + = t "bom_no_results" .margin-bottom-50{ 'ng-hide' => 'RequestMonitor.loading || filteredLineItems.length == 0' } %form{ name: 'bulk_order_form' } @@ -100,30 +114,43 @@ %th.bulk %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } %th.order_no{ 'ng-show' => 'columns.order_no.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.number'; reverse = !reverse" } Order No. + %a{ :href => '', 'ng-click' => "predicate = 'order.number'; reverse = !reverse" } + = t "order_no" %th.full_name{ 'ng-show' => 'columns.full_name.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.full_name'; reverse = !reverse" } Name + %a{ :href => '', 'ng-click' => "predicate = 'order.full_name'; reverse = !reverse" } + = t "name" %th.email{ 'ng-show' => 'columns.email.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.email'; reverse = !reverse" } Email + %a{ :href => '', 'ng-click' => "predicate = 'order.email'; reverse = !reverse" } + = t "email" %th.phone{ 'ng-show' => 'columns.phone.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.phone'; reverse = !reverse" } Phone + %a{ :href => '', 'ng-click' => "predicate = 'order.phone'; reverse = !reverse" } + = t "phone" %th.date{ 'ng-show' => 'columns.order_date.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.completed_at'; reverse = !reverse" } Order Date + %a{ :href => '', 'ng-click' => "predicate = 'order.completed_at'; reverse = !reverse" } + =t "bom_date" %th.producer{ 'ng-show' => 'columns.producer.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'supplier.name'; reverse = !reverse" } Producer + %a{ :href => '', 'ng-click' => "predicate = 'supplier.name'; reverse = !reverse" } + = t "producer" %th.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.order_cycle.name'; reverse = !reverse" } Order Cycle + %a{ :href => '', 'ng-click' => "predicate = 'order.order_cycle.name'; reverse = !reverse" } + = t "bom_cycle" %th.hub{ 'ng-show' => 'columns.hub.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.distributor.name'; reverse = !reverse" } Hub + %a{ :href => '', 'ng-click' => "predicate = 'order.distributor.name'; reverse = !reverse" } + = t "bom_hub" %th.variant{ 'ng-show' => 'columns.variant.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'units_variant.full_name'; reverse = !reverse" } Product: Unit - %th.quantity{ 'ng-show' => 'columns.quantity.visible' } Quantity - %th.max{ 'ng-show' => 'columns.max.visible' } Max - %th.final_weight_volume{ 'ng-show' => 'columns.final_weight_volume.visible' } Weight/Volume - %th.price{ 'ng-show' => 'columns.price.visible' } Price + %a{ :href => '', 'ng-click' => "predicate = 'units_variant.full_name'; reverse = !reverse" } + = t "bom_variant" + %th.quantity{ 'ng-show' => 'columns.quantity.visible' } + = t "products_quantity" + %th.max{ 'ng-show' => 'columns.max.visible' } + = t "shop_variant_quantity_max" + %th.final_weight_volume{ 'ng-show' => 'columns.final_weight_volume.visible' } + = t "weight_volume" + %th.price{ 'ng-show' => 'columns.price.visible' } + = t "products_price" %th.actions %th.actions - Ask?  + = t "ask" %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } diff --git a/app/views/spree/admin/overview/_enterprises_footer.html.haml b/app/views/spree/admin/overview/_enterprises_footer.html.haml index c61add38f5..c704dfef64 100644 --- a/app/views/spree/admin/overview/_enterprises_footer.html.haml +++ b/app/views/spree/admin/overview/_enterprises_footer.html.haml @@ -1,3 +1,3 @@ %a.sixteen.columns.alpha.button.bottom.blue{ href: "#{main_app.admin_enterprises_path}" } - MANAGE MY ENTERPRISES + = t "spree_admin_overview_enterprises_footer" %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_header.html.haml b/app/views/spree/admin/overview/_enterprises_header.html.haml index d198e8b549..5b1979c4ce 100644 --- a/app/views/spree/admin/overview/_enterprises_header.html.haml +++ b/app/views/spree/admin/overview/_enterprises_header.html.haml @@ -1,8 +1,10 @@ %div.header.sixteen.columns.alpha{ :class => "#{@enterprises.count > 0 ? "" : "red"}"} - %h3.thirteen.columns.alpha My Enterprises + %h3.thirteen.columns.alpha + = t "spree_admin_overview_enterprises_header" - if @enterprises.any? - if spree_current_user.can_own_more_enterprises? %a.three.columns.omega.icon-plus.button.blue.white-bottom{ href: "#{main_app.new_admin_enterprise_path}" } - CREATE NEW + = t "spree_admin_enterprises_create_new" - else - %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? + %a{ "ofn-with-tip" => "Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network." } + = t "admin_enterprise_groups_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 78c0e2427b..3073ec78a3 100644 --- a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml @@ -1,12 +1,15 @@ %div.hubs_tab{ ng: { show: "activeTab == 'hubs'"} } %div.sixteen.columns.alpha.list-title - %span.five.columns.alpha Name + %span.five.columns.alpha + = t "spree_admin_enterprises_hubs_name" - if can? :admin, Spree::PaymentMethod %span.centered.three.columns Payment Methods - if can? :admin, Spree::ShippingMethod - %span.centered.three.columns Shipping Methods + %span.centered.three.columns + = t "spree_admin_enterprises_shipping_methods" - if can? :admin, EnterpriseFee - %span.centered.three.columns Enterprise Fees + %span.centered.three.columns + = t "spree_admin_enterprises_fees" %div.sixteen.columns.alpha.list - @enterprises.is_distributor.each do |enterprise| %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } diff --git a/app/views/spree/admin/overview/_enterprises_none.html.haml b/app/views/spree/admin/overview/_enterprises_none.html.haml index c1428a6863..72cd90df1a 100644 --- a/app/views/spree/admin/overview/_enterprises_none.html.haml +++ b/app/views/spree/admin/overview/_enterprises_none.html.haml @@ -1,7 +1,8 @@ %div.sixteen.columns.alpha.list-item.red - %span.text.fifteen.columns.alpha You don't have any enterprises yet. + %span.text.fifteen.columns.alpha + = t "spree_admin_enterprises_none_text" %span.one.columns.omega %span.icon-remove-sign %a.sixteen.columns.alpha.button.bottom.red{ href: "#{main_app.new_admin_enterprise_path}" } - CREATE A NEW ENTERPRISE + = t "spree_admin_enterprises_none_create_a_new_enterprise" %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml index d5cad103b7..19cae1e6f8 100644 --- a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml @@ -1,11 +1,15 @@ %div.producers_tab{ ng: { show: "activeTab == 'producers'"} } %div.list-title.sixteen.columns.alpha - %span.five.columns.alpha Name + %span.five.columns.alpha + = t "spree_admin_enterprises_producers_name" - if can? :admin, Spree::Product - %span.centered.three.columns Total Products - %span.centered.three.columns Active Products + %span.centered.three.columns + = t "spree_admin_enterprises_producers_total_products" + %span.centered.three.columns + = t "spree_admin_enterprises_producers_active_products" - if can? :admin, OrderCycle - %span.centered.three.columns Products in OCs + %span.centered.three.columns + = t "spree_admin_enterprises_producers_order_cycles" %div.sixteen.columns.alpha.list - @enterprises.is_primary_producer.each do |enterprise| %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } diff --git a/app/views/spree/admin/overview/_enterprises_tabs.html.haml b/app/views/spree/admin/overview/_enterprises_tabs.html.haml index f6e5413786..5f8eb9e3c2 100644 --- a/app/views/spree/admin/overview/_enterprises_tabs.html.haml +++ b/app/views/spree/admin/overview/_enterprises_tabs.html.haml @@ -1,3 +1,5 @@ %div.sixteen.columns.alpha.tabs - %div.dashboard_tab.eight.columns.alpha.blue{ ng: { class: "{selected: activeTab == 'hubs'}", click: "activeTab = 'hubs'" } } HUBS - %div.dashboard_tab.eight.columns.omega.blue{ ng: { class: "{selected: activeTab == 'producers'}", click: "activeTab = 'producers'" } } PRODUCERS + %div.dashboard_tab.eight.columns.alpha.blue{ ng: { class: "{selected: activeTab == 'hubs'}", click: "activeTab = 'hubs'" } } + = t "spree_admin_enterprises_tabs_hubs" + %div.dashboard_tab.eight.columns.omega.blue{ ng: { class: "{selected: activeTab == 'producers'}", click: "activeTab = 'producers'" } } + = t "spree_admin_enterprises_tabs_producers" diff --git a/app/views/spree/admin/overview/_order_cycles.html.haml b/app/views/spree/admin/overview/_order_cycles.html.haml index 34b82c1f87..8628837a47 100644 --- a/app/views/spree/admin/overview/_order_cycles.html.haml +++ b/app/views/spree/admin/overview/_order_cycles.html.haml @@ -1,10 +1,13 @@ %div.dashboard_item.seven.columns.omega#order_cycles %div.header.seven.columns.alpha{ :class => "#{@order_cycle_count > 0 ? "" : "orange"}"} - %h3.four.columns.alpha Order Cycles + %h3.four.columns.alpha + = t "spree_admin_order_cycles" - if @order_cycle_count > 0 %a.three.columns.omega.icon-plus.button.blue{ href: "#{main_app.new_admin_order_cycle_path}" } - CREATE NEW + = t "spree_admin_enterprises_create_new" - else + %a.with-tip{ title: t(:spree_admin_order_cycles_tip) } + = t "admin.whats_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 @@ -14,13 +17,16 @@ %span.one.column.omega %span.icon-ok-sign %a.seven.columns.alpha.button.bottom.blue{ href: "#{main_app.admin_order_cycles_path}" } - MANAGE ORDER CYCLES + = t "spree_admin_enterprises_producers_manage_order_cycles" %span.icon-arrow-right - else %div.seven.columns.alpha.list-item.orange - %span.six.columns.alpha You don't have any active order cycles. + %span.six.columns.alpha + = t "spree_admin_enterprises_producers_orders_cycle_text" %span.one.column.omega %span.icon-warning-sign %a.seven.columns.alpha.button.bottom.orange{ href: "#{main_app.admin_order_cycles_path}" } + = t "spree_admin_enterprises_producers_manage_order_cycles" + %span.icon-arrow-right MANAGE ORDER CYCLES %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 24b60250c3..c017cd11f9 100644 --- a/app/views/spree/admin/overview/_products.html.haml +++ b/app/views/spree/admin/overview/_products.html.haml @@ -1,11 +1,13 @@ %div.dashboard_item.seven.columns.alpha#products %div.header.seven.columns.alpha{ :class => "#{@product_count > 0 ? "" : "red"}"} - %h3.four.columns.alpha Products + %h3.four.columns.alpha + = t "products" - if @product_count > 0 %a.three.columns.omega.icon-plus.button.blue{ href: "#{new_admin_product_path}" } - CREATE NEW + = t "spree_admin_enterprises_create_new" - else - %a{ "ofn-with-tip" => "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." } + = t "admin.whats_this" %div.seven.columns.alpha.list - if @product_count > 0 %div.seven.columns.alpha.list-item @@ -14,13 +16,14 @@ %span.one.column.omega %span.icon-ok-sign %a.seven.columns.alpha.button.bottom.blue{ href: "#{bulk_edit_admin_products_path}" } - MANAGE PRODUCTS + = t "spree_admin_enterprises_producers_manage_products" %span.icon-arrow-right - else %div.seven.columns.alpha.list-item.red - %span.six.columns.alpha You don't have any active products. + %span.six.columns.alpha + = t "spree_admin_enterprises_any_active_products_text" %span.one.column.omega %span.icon-remove-sign %a.seven.columns.alpha.button.bottom.red{ href: "#{new_admin_product_path}" } - CREATE A NEW PRODUCT + = t "spree_admin_enterprises_create_new_product" %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_unconfirmed.html.haml b/app/views/spree/admin/overview/_unconfirmed.html.haml index 05c4df2a0d..5ff0e17a84 100644 --- a/app/views/spree/admin/overview/_unconfirmed.html.haml +++ b/app/views/spree/admin/overview/_unconfirmed.html.haml @@ -1,4 +1,4 @@ - @enterprises.unconfirmed.each do |enterprise| .alert - %h6= "Action Required: Please confirm the email address for #{enterprise.name}." - %span.message= "We've sent a confirmation email to #{enterprise.email}, so please check there for further instructions. Thanks!" \ No newline at end of file + %h6= "#{t :spree_admin_overview_action_required}: #{t :spree_admin_single_enterprise_alert_mail_confirmation} #{enterprise.name}." + %span.message= "#{t :spree_admin_single_enterprise_alert_mail_sent} #{enterprise.email}. #{t :spree_admin_overview_check_your_inbox}" 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 511718d3f4..0647304e05 100644 --- a/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml @@ -3,7 +3,8 @@ %div{ 'ng-app' => 'ofn.admin' } - %h1{ :style => 'margin-bottom: 30px' } Dashboard + %h1{ :style => 'margin-bottom: 30px' } + = t "dashbord" - if @enterprises.unconfirmed.any? diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 3998f736de..91c0934e21 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -19,7 +19,7 @@ } #package_button %button#toggle_type{ onClick: 'toggleType()' } - Change Package + = t "change_package" %i.icon-chevron-down @@ -29,17 +29,17 @@ - if @enterprise.confirmed_at.nil? .alert-box - Please confirm the email address for + = t "spree_admin_single_enterprise_alert_mail_confirmation" %strong= "#{@enterprise.name}." - We've sent an email to + = t "spree_admin_single_enterprise_alert_mail_sent" %strong= "#{@enterprise.email}." - = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.email } ), method: :post) + = link_to(t('resend'), main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: @enterprise.email } ), method: :post) %a.close{ href: "#" } × - if !@enterprise.visible .alert-box - %strong Hint: - To allow people to find you, turn on your visibility under - %strong= "Manage #{@enterprise.name}." + %strong + = t "spree_admin_single_enterprise_hint" + %strong= "#{t 'manage'} #{@enterprise.name}." %a.close{ href: "#" } × .row @@ -47,14 +47,15 @@ .header %h3 %span.icon-map-marker - Your profile live - %p on the Open Food Network map + = t "your_profil_live" + %p + = t "on_ofn_map" .list /-# Can we pass an anchor here to zoom to our enterprise? %a.button.bottom{href: main_app.map_path, target: '_blank'} - See + = t "see" = @enterprise.name - live + = t "live" %span.icon-arrow-right .two.columns   @@ -63,11 +64,12 @@ .header %h3 %span.icon-edit - Edit profile details - %p Change your profile description, images, etc. + = t "edit_profile_details" + %p + = t "edit_profile_details_etc" .list %a.button.bottom{href: main_app.edit_admin_enterprise_path(@enterprise)} - Manage + = t "manage" = @enterprise.name %span.icon-arrow-right @@ -77,10 +79,10 @@ .header %h3 %span.icon-th-large - Add & manage products + = t "add_and_manage_products" .list %a.button.bottom{href: bulk_edit_admin_products_path} - Manage products + = t "manage_products" %span.icon-arrow-right .two.columns @@ -91,8 +93,8 @@ .header %h3 %span.icon-shopping-cart - Add & manage order cycles + = t "add_and_manage_order_cycles" .list %a.button.bottom{href: main_app.admin_order_cycles_path} - Manage order cycles + = t "manage_order_cycles" %span.icon-arrow-right diff --git a/app/views/spree/admin/products/bulk_edit/_filters.html.haml b/app/views/spree/admin/products/bulk_edit/_filters.html.haml index 3e676ede6d..4ce0c7c053 100644 --- a/app/views/spree/admin/products/bulk_edit/_filters.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_filters.html.haml @@ -4,11 +4,11 @@ %br %input.quick-search.fullwidth{ 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } .filter_select.four.columns - %label{ :for => 'producer_filter' }Producer + %label{ :for => 'producer_filter' }= t 'producer' %br %select.fullwidth{ :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in filterProducers' } .filter_select.four.columns - %label{ :for => 'category_filter' }Category + %label{ :for => 'category_filter' }= t 'category' %br %select.fullwidth{ :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in filterTaxons'} %div{ :class => "one column" }   diff --git a/app/views/spree/admin/reports/customers.html.haml b/app/views/spree/admin/reports/customers.html.haml index aef51702a6..3ab25e5c6c 100644 --- a/app/views/spree/admin/reports/customers.html.haml +++ b/app/views/spree/admin/reports/customers.html.haml @@ -2,30 +2,30 @@ %br .row .four.columns.alpha - = label_tag nil, "Distributor: " + = label_tag nil, t(:report_customers_distributor) = select_tag(:distributor_id, options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), {:include_blank => true, :class => "select2 fullwidth"}) .four.columns - = label_tag nil, "Supplier: " + = label_tag nil, t(:report_customers_supplier) = select_tag(:supplier_id, options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), {:include_blank => true, :class => "select2 fullwidth"}) .six.columns - = label_tag nil, "Order Cycle: " + = label_tag nil, t(:report_customers_cycle) = select_tag(:order_cycle_id, options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), {:include_blank => true, :class => "select2 fullwidth"}) - = label_tag nil, "Report Type: " + = label_tag nil, t(:report_customers_type) = select_tag(:report_type, options_for_select(@report_types, @report_type)) %br %br = check_box_tag :csv - = label_tag :csv, "Download as csv" + = label_tag :csv, t(:report_customers_csv) %br = button t(:search) @@ -44,4 +44,3 @@ - if @report.table.empty? %tr %td{:colspan => "2"}= t(:none) - diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml index 5ab379a669..808c1ead61 100644 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -2,16 +2,16 @@ = render 'date_range_form', f: f .row - .alpha.two.columns= label_tag nil, "Hubs: " + .alpha.two.columns= label_tag nil, t(:report_hubs) .omega.fourteen.columns= f.collection_select(:distributor_id_in, @distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) .row - .alpha.two.columns= label_tag nil, "Order Cycles: " + .alpha.two.columns= label_tag nil, t(:report_customers_cycle) .omega.fourteen.columns = f.select(:order_cycle_id_in, report_order_cycle_options(@order_cycles), {selected: params[:q][:order_cycle_id_in]}, {class: "select2 fullwidth", multiple: true}) .row - .alpha.two.columns= label_tag nil, "Payment Methods: " + .alpha.two.columns= label_tag nil, t(:report_payment) .omega.fourteen.columns= select_tag(:payment_method_in, options_for_select(report_payment_method_options(@report.orders), params[:payment_method_in]), {class: "select2 fullwidth", multiple: true}) .row @@ -24,7 +24,7 @@ .row = check_box_tag :csv - = label_tag :csv, "Download as csv" + = label_tag :csv, t(:report_customers_csv) .row = button t(:search) diff --git a/app/views/spree/admin/reports/orders_and_distributors.html.haml b/app/views/spree/admin/reports/orders_and_distributors.html.haml index 23baad395e..aa710b1db6 100644 --- a/app/views/spree/admin/reports/orders_and_distributors.html.haml +++ b/app/views/spree/admin/reports/orders_and_distributors.html.haml @@ -2,7 +2,7 @@ = render 'date_range_form', f: f = check_box_tag :csv - = label_tag :csv, "Download as csv" + = label_tag :csv, t(:report_customers_csv) %br = button t(:search) @@ -10,7 +10,7 @@ %br %table#listing_orders.index %thead - %tr{'data-hook' => "orders_header"} + %tr{'data-hook' => 'orders_header'} - @report.header.each do |heading| %th=heading %tbody @@ -21,4 +21,3 @@ - if @report.table.empty? %tr %td{:colspan => "2"}= t(:none) - diff --git a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml index 2465fe0ff2..5bda882108 100644 --- a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml +++ b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml @@ -2,25 +2,25 @@ = render 'date_range_form', f: f .row - .alpha.two.columns= label_tag nil, "Hubs: " + .alpha.two.columns= label_tag nil, t(:report_hubs) .omega.fourteen.columns= f.collection_select(:distributor_id_in, @distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) .row - .alpha.two.columns= label_tag nil, "Producers: " + .alpha.two.columns= label_tag nil, t(:report_producers) .omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true}) .row - .alpha.two.columns= label_tag nil, "Order Cycles: " + .alpha.two.columns= label_tag nil, t(:report_customers_cycle) .omega.fourteen.columns = f.select(:order_cycle_id_in, report_order_cycle_options(@order_cycles), {selected: params[:q][:order_cycle_id_in]}, {class: "select2 fullwidth", multiple: true}) .row - .alpha.two.columns= label_tag nil, "Report Type: " + .alpha.two.columns= label_tag nil, t(:report_type) .omega.fourteen.columns= select_tag(:report_type, options_for_select(@report_types, @report_type)) .row = check_box_tag :csv - = label_tag :csv, "Download as csv" + = label_tag :csv, t(:report_customers_csv) .row = button t(:search) diff --git a/app/views/spree/admin/reports/packing.html.haml b/app/views/spree/admin/reports/packing.html.haml index 2aa8d8c8b9..fa9d22eb70 100644 --- a/app/views/spree/admin/reports/packing.html.haml +++ b/app/views/spree/admin/reports/packing.html.haml @@ -2,25 +2,25 @@ = render 'date_range_form', f: f .row - .alpha.two.columns= label_tag nil, "Hubs: " + .alpha.two.columns= label_tag nil, t(:report_hubs) .omega.fourteen.columns= f.collection_select(:distributor_id_in, @distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) .row - .alpha.two.columns= label_tag nil, "Producers: " + .alpha.two.columns= label_tag nil, t(:report_producers) .omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true}) .row - .alpha.two.columns= label_tag nil, "Order Cycles: " + .alpha.two.columns= label_tag nil, t(:report_customers_cycle) .omega.fourteen.columns = f.select(:order_cycle_id_in, report_order_cycle_options(@order_cycles), {selected: params[:q][:order_cycle_id_in]}, {class: "select2 fullwidth", multiple: true}) .row - .alpha.two.columns= label_tag nil, "Report Type: " + .alpha.two.columns= label_tag nil, t(:report_type) .omega.fourteen.columns= select_tag(:report_type, options_for_select(@report_types, @report_type)) .row = check_box_tag :csv - = label_tag :csv, "Download as csv" + = label_tag :csv, t(:report_customers_csv) .row = button t(:search) diff --git a/app/views/spree/admin/reports/payments.html.haml b/app/views/spree/admin/reports/payments.html.haml index 909846f47f..b69708fbd1 100644 --- a/app/views/spree/admin/reports/payments.html.haml +++ b/app/views/spree/admin/reports/payments.html.haml @@ -3,15 +3,15 @@ .row .four.columns.alpha - = label_tag nil, "Distributor: " - = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) - = label_tag nil, "Report Type: " + = label_tag nil, t(:report_distributor) + = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => t(:report_all)}, {:class => "select2 fullwidth"}) + = label_tag nil, t(:report_customers_type) %br - = select_tag(:report_type, options_for_select([['Payments By Type',:payments_by_payment_type],['Itemised Payment Totals',:itemised_payment_totals],['Payment Totals',:payment_totals]], @report_type)) + = select_tag(:report_type, options_for_select([[t(:report_payment_by),:payments_by_payment_type],[t(:report_itemised_payment),:itemised_payment_totals],[t(:report_payment_totals),:payment_totals]], @report_type)) %br %br = check_box_tag :csv - = label_tag :csv, "Download as csv" + = label_tag :csv, t(:report_customers_csv) %br %br = button t(:search) diff --git a/app/views/spree/admin/reports/products_and_inventory.html.haml b/app/views/spree/admin/reports/products_and_inventory.html.haml index c3470e6047..40c4fbfccf 100644 --- a/app/views/spree/admin/reports/products_and_inventory.html.haml +++ b/app/views/spree/admin/reports/products_and_inventory.html.haml @@ -2,33 +2,33 @@ %br .row .four.columns.alpha - = label_tag nil, "Distributor: " + = label_tag nil, t(:report_distributor) = select_tag(:distributor_id, options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), {:include_blank => true, :class => "select2 fullwidth"}) - + .four.columns - = label_tag nil, "Supplier: " + = label_tag nil, t(:report_customers_supplier) = select_tag(:supplier_id, options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), {:include_blank => true, :class => "select2 fullwidth"}) - + .six.columns - = label_tag nil, "Order Cycle: " + = label_tag nil, t(:report_order_cycle) = select_tag(:order_cycle_id, options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), {:include_blank => true, :class => "select2 fullwidth"}) - = label_tag nil, "Report Type: " + = label_tag nil, t(:report_type) %br = select_tag(:report_type, options_for_select(@report_types, params[:report_type])) %br %br = check_box_tag :csv - = label_tag :csv, "Download as csv" + = label_tag :csv, t(:report_customers_csv) %br = button t(:search) %br diff --git a/app/views/spree/admin/reports/sales_tax.html.haml b/app/views/spree/admin/reports/sales_tax.html.haml index b0a115a74b..9a5cb9e533 100644 --- a/app/views/spree/admin/reports/sales_tax.html.haml +++ b/app/views/spree/admin/reports/sales_tax.html.haml @@ -3,10 +3,10 @@ .row .four.columns.alpha - = label_tag nil, "Distributor:" + = label_tag nil, t(:report_distributor) = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) = check_box_tag :csv - = label_tag :csv, "Download as csv" + = label_tag :csv, t(:report_customers_csv) %br = button t(:search) diff --git a/app/views/spree/admin/reports/users_and_enterprises.html.haml b/app/views/spree/admin/reports/users_and_enterprises.html.haml index 6820ee04b1..1b57373c38 100644 --- a/app/views/spree/admin/reports/users_and_enterprises.html.haml +++ b/app/views/spree/admin/reports/users_and_enterprises.html.haml @@ -1,10 +1,10 @@ = form_tag spree.users_and_enterprises_admin_reports_url do |f| .row - .alpha.two.columns= label_tag nil, "Enterprises: " + .alpha.two.columns= label_tag nil, t(:report_entreprises) .omega.fourteen.columns= select_tag(:enterprise_id_in, options_from_collection_for_select(Enterprise.all, :id, :name, params[:enterprise_id_in].andand.split(",")), {class: "select2 fullwidth", multiple: true}) .row - .alpha.two.columns= label_tag nil, "Users: " + .alpha.two.columns= label_tag nil, t(:report_users) .omega.fourteen.columns= select_tag(:user_id_in, options_from_collection_for_select(Spree::User.all, :id, :email, params[:user_id_in].andand.split(",")), {class: "select2 fullwidth", multiple: true}) -# Might need this later if we add different kinds of reports @@ -14,7 +14,7 @@ .row = check_box_tag :csv - = label_tag :csv, "Download as csv" + = label_tag :csv, t(:report_customers_csv) .row = button t(:search) %br diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml index eca51fed4f..8669089f81 100644 --- a/app/views/spree/admin/reports/xero_invoices.html.haml +++ b/app/views/spree/admin/reports/xero_invoices.html.haml @@ -2,31 +2,31 @@ = render 'date_range_form', f: f .row - .four.columns.alpha= label_tag :report_type, "Report Type: " + .four.columns.alpha= label_tag :report_type, t(:report_type) .four.columns.omega= select_tag :report_type, options_for_select(xero_report_types, params[:report_type]), {include_blank: false, class: "select2 fullwidth"} .row - .four.columns.alpha= label_tag nil, "Hub: " + .four.columns.alpha= label_tag nil, t(:report_hubs) .four.columns.omega= f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) .row - .four.columns.alpha= label_tag nil, "Order Cycle: " + .four.columns.alpha= label_tag nil, t(:report_order_cycle) .four.columns.omega= f.select(:order_cycle_id_eq, options_for_select(report_order_cycle_options(@order_cycles), params[:q][:order_cycle_id_eq]), {:include_blank => true}, {:class => "select2 fullwidth"}) .row - .four.columns.alpha= label_tag :initial_invoice_number, "Initial invoice number:" + .four.columns.alpha= label_tag :initial_invoice_number, t(:initial_invoice_number) .twelve.columns.omega= text_field_tag :initial_invoice_number, params[:initial_invoice_number] .row - .four.columns.alpha= label_tag :invoice_date, "Invoice date:" + .four.columns.alpha= label_tag :invoice_date, t(:invoice_date) .twelve.columns.omega= text_field_tag :invoice_date, params[:invoice_date], class: 'datetimepicker' .row - .four.columns.alpha= label_tag :due_date, "Due date:" + .four.columns.alpha= label_tag :due_date, t(:due_date) .twelve.columns.omega= text_field_tag :due_date, params[:due_date], class: 'datetimepicker' .row - .four.columns.alpha= label_tag :account_code, "Account code:" + .four.columns.alpha= label_tag :account_code, t(:account_code) .twelve.columns.omega= text_field_tag :account_code, params[:account_code] .row - .four.columns.alpha= label_tag :csv, "Download as CSV:" + .four.columns.alpha= label_tag :csv, t(:report_customers_csv) .twelve.columns.omega= check_box_tag :csv .row .four.columns.alpha= button t(:search) diff --git a/app/views/spree/admin/shared/_address_form_simple.html.haml b/app/views/spree/admin/shared/_address_form_simple.html.haml index 3b8f756d88..36f49bf384 100644 --- a/app/views/spree/admin/shared/_address_form_simple.html.haml +++ b/app/views/spree/admin/shared/_address_form_simple.html.haml @@ -1,18 +1,24 @@ %tr{"data-hook" => "address1"} - %td Address: + %td + = t(:admin_shared_address_1): %td= f.text_field :address1 -%tr{"data-hook" => "address2"} - %td Address (cont.): +%tr{"data-hook" => "address2" } + %td + = t(:admin_shared_address_2): %td= f.text_field :address2 -%tr{"data-hook" => "city"} - %td City: +%tr{"data-hook" => "city" } + %td + = t(:admin_share_city): %td= f.text_field :city -%tr{"data-hook" => "zipcode"} - %td Postcode: +%tr{"data-hook" => "zipcode" } + %td + = t(:admin_share_zipcode): %td= f.text_field :zipcode -%tr{"data-hook" => "country"} - %td Country: +%tr{"data-hook" => "country" } + %td + = t(:admin_share_country): %td= f.collection_select(:country_id, available_countries, :id, :name) -%tr{"data-hook" => "state"} - %td State: +%tr{"data-hook" => "state" } + %td + = t(:admin_share_state): %td= f.collection_select(:state_id, f.object.country.states, :id, :name) diff --git a/app/views/spree/admin/shared/_hubs_sidebar.html.haml b/app/views/spree/admin/shared/_hubs_sidebar.html.haml index 23c536402b..75ef9bb32f 100644 --- a/app/views/spree/admin/shared/_hubs_sidebar.html.haml +++ b/app/views/spree/admin/shared/_hubs_sidebar.html.haml @@ -1,8 +1,9 @@ - hubs_color = @hubs.count > 0 ? "blue" : "red" -- hubs_color = 'red' if (controller.action_name == 'create' || controller.action_name == 'update') && @object.errors.full_messages.include?("At least one hub must be selected") +- hubs_color = 'red' if (controller.action_name == 'create' || controller.action_name == 'update') && @object.errors.full_messages.include?(t(:hub_sidebar_at_least)) .sidebar_item.omega.four.columns#hubs .four.columns.alpha.header{ class: "#{hubs_color}" } - %span.four.columns.alpha.centered Hubs + %span.four.columns.alpha.centered + = t(:hub_sidebar_hubs) .four.columns.alpha.list{ class: "#{hubs_color}" } - if @hubs.count > 0 = hidden_field klass, :distributor_ids, :multiple => true, value: nil @@ -17,9 +18,10 @@ %span.icon-arrow-right - else .four.columns.alpha.list-item - %span.three.columns.alpha None Available + %span.three.columns.alpha + = t(:hub_sidebar_none_available) %span.one.column.omega %span.icon-remove-sign %a.four.columns.alpha.button{ href: "#{main_app.admin_enterprises_path}", class: "#{hubs_color}" } - MANAGE + = t(:hub_sidebar_manage) %span.icon-arrow-right diff --git a/app/views/spree/admin/variants/_autocomplete.js.erb b/app/views/spree/admin/variants/_autocomplete.js.erb index 7b52c0a716..41b0e84550 100644 --- a/app/views/spree/admin/variants/_autocomplete.js.erb +++ b/app/views/spree/admin/variants/_autocomplete.js.erb @@ -17,7 +17,7 @@ {{#if variant.option_values}} diff --git a/app/views/spree/users/_fat.html.haml b/app/views/spree/users/_fat.html.haml new file mode 100644 index 0000000000..c10cf8254a --- /dev/null +++ b/app/views/spree/users/_fat.html.haml @@ -0,0 +1,29 @@ +.row{"ng-if" => "open()"} + .columns.small-12.fat + %table + %tr + %th.order1= t :transaction + %th.order2= t :transaction_date + %th.order3.show-for-large-up= t :payment_state + %th.order4.show-for-large-up= t :shipping_state + %th.order5.text-right= t :value + %th.order6.text-right.show-for-large-up= t :outstanding_balance + %th.order7.text-right= t :running_balance + %tbody.transaction-group{"ng-repeat" => "order in distributor.distributed_orders", "ng-class-odd"=>"'odd'", "ng-class-even"=>"'even'"} + %tr.order-row + %td.order1 + %a{"bo-href" => "order.path", "bo-text" => "('order' | t )+ ' ' + order.number"} + %td.order2{"bo-text" => "order.completed_at"} + %td.order3.show-for-large-up{"bo-text" => "'spree.payment_states.' + order.payment_state | t | capitalize"} + %td.order4.show-for-large-up{"bo-text" => "'spree.shipment_states.' + order.shipment_state | t | capitalize"} + %td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","bo-text" => "order.total | localizeCurrency"} + %td.order6.text-right.show-for-large-up{"ng-class" => "{'credit' : order.outstanding_balance < 0, 'debit' : order.outstanding_balance > 0, 'paid' : order.outstanding_balance == 0}", "bo-text" => "order.outstanding_balance | localizeCurrency"} + %td.order7.text-right{"ng-class" => "{'credit' : order.running_balance < 0, 'debit' : order.running_balance > 0, 'paid' : order.running_balance == 0}", "bo-text" => "order.running_balance | localizeCurrency"} + %tr.payment-row{"ng-repeat" => "payment in order.payments"} + %td.order1= t :payment + %td.order2{"bo-text" => "payment.updated_at"} + %td.order3.show-for-large-up{"bo-text" => "payment.payment_method"} + %td.order4.show-for-large-up + %td.order5.text-right{"ng-class" => "{'credit' : payment.amount > 0, 'debit' : payment.amount < 0, 'paid' : payment.amount == 0}","bo-text" => "payment.amount | localizeCurrency"} + %td.order6.show-for-large-up + %td.order7 diff --git a/app/views/spree/users/_skinny.html.haml b/app/views/spree/users/_skinny.html.haml new file mode 100644 index 0000000000..14c04f024a --- /dev/null +++ b/app/views/spree/users/_skinny.html.haml @@ -0,0 +1,12 @@ +.row.active_table_row.skinny-head.margin-top{"ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open()}"} + .columns.small-2 + %span.margin-top + %img.account-logo{"logo-fallback" => true, "ng-src" => "{{distributor.logo}}"} + .columns.small-10.medium-5 + %span.margin-top + %strong{"bo-text" => "distributor.name"} + .columns.small-8.small-offset-2.medium-3.text-right + %span.margin-top.distributor-balance{"bo-text" => "distributor.balance | formatBalance", "ng-class" => "{'credit' : distributor.balance < 0, 'debit' : distributor.balance > 0, 'paid' : distributor.balance == 0}" } + .columns.small-2.medium-2.text-right + %span.margin-top + %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} diff --git a/app/views/spree/users/show.html.haml b/app/views/spree/users/show.html.haml index 878d271e42..83171daf0d 100644 --- a/app/views/spree/users/show.html.haml +++ b/app/views/spree/users/show.html.haml @@ -1,34 +1,24 @@ .darkswarm + = inject_orders_by_distributor + .row.pad-top .small-12.columns.pad-top - %h1= accurate_title + %h2= accurate_title .account-summary{"data-hook" => "account_summary"} - %dl#user-info - %dt= t(:email) - %dd - = @user.email - (#{link_to t(:edit), spree.edit_account_path}) - .account-my-orders{"data-hook" => "account_my_orders"} - %h3= t(:my_orders) - - if @orders.present? - %table.order-summary - %thead - %tr - %th.order-number= t(:order_number) - %th.order-date= t(:order_date) - %th.order-status= t(:status) - %th.order-payment-state= t(:payment_state) - %th.order-shipment-state= t(:shipment_state) - %th.order-total= t(:total) - %tbody - - @orders.each do |order| - %tr{class: cycle('even', 'odd')} - %td.order-number= link_to order.number, order_url(order) - %td.order-date= l order.completed_at.to_date - %td.order-status= t(order.state).titleize - %td.order-payment-state= t("payment_states.#{order.payment_state}") if order.payment_state - %td.order-shipment-state= t("shipment_states.#{order.shipment_state}") if order.shipment_state - %td.order-total= money order.total - - else - %p= t(:you_have_no_orders_yet) - %br/ + = @user.email + (#{link_to t(:edit), spree.edit_account_path}) + %h3= t(:my_orders) + .orders{"ng-controller" => "OrdersCtrl", "ng-cloak" => true} + .row{bindonce: true} + .small-12.columns + .active_table + %distributor.active_table_node.row.animate-repeat{"ng-if" => "Orders.orders_by_distributor.length > 0", "ng-repeat" => "(key, distributor) in Orders.orders_by_distributor", + "ng-controller" => "DistributorNodeCtrl", + "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !distributor.active}", + id: "{{distributor.hash}}"} + .small-12.columns + = render partial: "spree/users/skinny" + = render partial: "spree/users/fat" + .message{"ng-if" => "Orders.orders_by_distributor.length == 0", "bo-text" => "'you_have_no_orders_yet' | t"} + + = render partial: "shared/footer" diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb new file mode 100644 index 0000000000..08d8aa67fc --- /dev/null +++ b/config/initializers/acts_as_taggable_on.rb @@ -0,0 +1 @@ +ActsAsTaggableOn.force_lowercase = true diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index cf8e7e78dc..58441e9934 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -10,23 +10,140 @@ en-GB: password: confirmation: you have successfully registered too_short: pick a longer name + # Overridden here due to a bug in spree i18n (Issue #870) + attributes: + spree/order: + payment_state: Payment State + shipment_state: Shipment State + devise: failure: invalid: | Invalid email or password. Were you a guest last time? Perhaps you need to create an account or reset your password. + enterprise_confirmations: + enterprise: + confirmed: Thankyou, your email address has been confirmed. + not_confirmed: Your email address could not be confirmed. Perhaps you have already completed this step? + confirmation_sent: "Confirmation email sent!" + confirmation_not_sent: "Could not send a confirmation email." home: "OFN" title: Open Food Network welcome_to: 'Welcome to ' + site_meta_description: "We begin from the ground up. With farmers and growers ready to tell their stories proudly and truly. With distributors ready to connect people with products fairly and honestly. With buyers who believe that better weekly shopping decisions can…" search_by_name: Search by name... producers: UK Producers producers_join: UK producers are now welcome to join Open Food Network UK. - charges_sales_tax: Charges sales tax? - print: "Print" + charges_sales_tax: Charges VAT? + print_invoice: "Print Invoice" + send_invoice: "Send Invoice" + resend_confirmation: "Resend Confirmation" + view_order: "View Order" + edit_order: "Edit Order" + ship_order: "Ship Order" + cancel_order: "Cancel Order" + confirm_send_invoice: "An invoice for this order will be sent to the customer. Are you sure you want to continue?" + confirm_resend_order_confirmation: "Are you sure you want to resend the order confirmation email?" + invoice: "Invoice" + percentage_of_sales: "%{percentage} of sales" + percentage_of_turnover: "Percentage of turnover" + monthly_cap_excl_tax: "monthly cap (excl. VAT)" + capped_at_cap: "capped at %{cap}" + per_month: "per month" + free: "free" + plus_tax: "plus GST" + total_monthly_bill_incl_tax: "Total Monthly Bill (Incl. Tax)" + say_no: "No" + say_yes: "Yes" - logo: "Logo (640x130)" - logo_mobile: "Mobile logo (75x26)" - logo_mobile_svg: "Mobile logo (SVG)" + 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}" + + whats_this: What's this? + + customers: + index: + add_customer: "Add customer" + customer_placeholder: "customer@example.org" + 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 + 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! + + + order_cycle: + choose_products_from: "Choose Products From:" + + enterprise: + select_outgoing_oc_products_from: Select outgoing OC products from + + enterprises: + form: + primary_details: + shopfront_requires_login: "Shopfront requires login?" + shopfront_requires_login_tip: "Choose whether customers must login to view the shopfront." + shopfront_requires_login_false: "Public" + shopfront_requires_login_true: "Require customers to login" + + home: + hubs: + show_closed_shops: "Show closed shops" + hide_closed_shops: "Hide closed shops" + show_on_map: "Show all on the map" + shared: + register_call: + selling_on_ofn: "Interested in getting on the Open Food Network?" + register: "Register here" + shop: + messages: + login: "login" + register: "register" + contact: "contact" + require_customer_login: "This shop is for customers only." + require_login_html: "Please %{login} if you have an account already. Otherwise, %{register} to become a customer." + require_customer_html: "Please %{contact} %{enterprise} to become a customer." + + # Printable Invoice Columns + invoice_column_item: "Item" + invoice_column_qty: "Qty" + invoice_column_tax: "VAT" + invoice_column_price: "Price" + + logo: "Logo (640x130)" #FIXME + logo_mobile: "Mobile logo (75x26)" #FIXME + logo_mobile_svg: "Mobile logo (SVG)" #FIXME home_hero: "Hero image" home_show_stats: "Show statistics" footer_logo: "Logo (220x76)" @@ -40,11 +157,10 @@ en-GB: footer_links_md: "Links" footer_about_url: "About URL" footer_tos_url: "Terms of Service URL" - invoice: "Invoice" name: Name - first_name: First name - last_name: Last name + first_name: First Name + last_name: Last Name email: Email phone: Phone next: Next @@ -84,9 +200,9 @@ en-GB: cart_empty: "Cart empty" cart_edit: "Edit your cart" - card_number: Card number - card_securitycode: "Security code" - card_expiry_date: Expiry date + card_number: Card Number + card_securitycode: "Security Code" + card_expiry_date: Expiry Date ofn_cart_headline: "Current cart for:" ofn_cart_distributor: "Distributor:" @@ -181,7 +297,7 @@ en-GB: checkout_cart_total: Cart total checkout_shipping_price: Shipping checkout_total_price: Total - checkout_back_to_cart: "Back to cart" + checkout_back_to_cart: "Back to Cart" order_paid: PAID order_not_paid: NOT PAID @@ -191,12 +307,32 @@ en-GB: order_delivery_on: Delivery on order_delivery_address: Delivery address order_special_instructions: "Your notes:" - order_pickup_instructions: Collection instructions + order_pickup_time: Ready for collection + order_pickup_instructions: Collection Instructions order_produce: Produce order_total_price: Total order_includes_tax: (includes tax) order_payment_paypal_successful: Your payment via PayPal has been processed successfully. - order_hub_info: Hub info + order_hub_info: Hub Info + + bom_tip: "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." + bom_shared: "Shared Resource?" + bom_page_title: "Bulk Order Management" + bom_no: "Order no." + bom_date: "Order date" + bom_cycle: "Order cycle" + bom_max: "Max" + bom_hub: "Hub" + bom_variant: "Product: Unit" + bom_final_weigth_volume: "Weight/Volume" + bom_quantity: "Quantity" + bom_actions_delete: "Delete Selected" + bom_loading: "Loading orders" + bom_no_results: "No orders found." + bom_order_error: "Some errors must be resolved before you can update orders.\nAny fields with red borders contain errors." + + unsaved_changes_warning: "Unsaved changes exist and will be lost if you continue." + unsaved_changes_error: "Fields with red borders contain errors." products: "Products" products_in: "in %{oc}" @@ -301,6 +437,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using products_cart_empty: "Cart empty" products_edit_cart: "Edit your cart" products_from: from + products_change: "No changes to save." + products_update_error: "Saving failed with the following error(s):" + products_update_error_msg: "Saving failed." + products_update_error_data: "Save failed due to invalid data:" + products_changes_saved: "Changes saved." search_no_results_html: "Sorry, no results found for %{query}. Try another search?" @@ -311,9 +452,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using groups_title: Groups groups_headline: Groups / regions + groups_text: "Every producer is unique. Every business has something different to offer. Our groups are collectives of producers, hubs and distributors who share something in common like location, farmers market or philosophy. This makes your shopping experience easier. So explore our groups and have the curating done for you." groups_search: "Search name or keyword" groups_no_groups: "No groups found" groups_about: "About Us" + groups_producers: "Our producers" groups_hubs: "Our hubs" groups_contact_web: Contact @@ -378,9 +521,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using ocs_close_time: "ORDERS CLOSE" ocs_when_headline: When do you want your order? ocs_when_text: No products are displayed until you select a date. - ocs_when_closing: "Closing on" + ocs_when_closing: "Closing On" ocs_when_choose: "Choose Order Cycle" - ocs_list: "List view" + ocs_list: "List View" producers_about: About us producers_buy: Shop for @@ -401,13 +544,26 @@ See the %{link} to find out more about %{sitename}'s features and to start using producers_signup_cta_headline: Join now! producers_signup_cta_action: Join now producers_signup_detail: Here's the detail. + producer: Producer products_item: Item products_description: Description products_variant: Variant products_quantity: Quantity products_availabel: Available? - products_price: Price + products_producer: "Producer" + products_price: "Price" + products_sku: "SKU" + products_name: "name" + products_unit: "unit" + products_on_hand: "on hand" + products_on_demand: "On demand?" + products_category: "Category" + products_tax_category: "tax category" + products_available_on: "Available On" + products_inherit: "Inherit?" + products_inherits_properties: "Inherits Properties?" + products_stock_level_reset: "Enable Stock Level Reset?" register_title: Register @@ -425,7 +581,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using shops_signup_detail: Here's the detail. orders_fees: Fees... - orders_edit_title: Shopping cart + orders_edit_title: Shopping Cart orders_edit_headline: Your shopping cart orders_edit_time: Order ready for orders_edit_continue: Continue shopping @@ -503,18 +659,17 @@ See the %{link} to find out more about %{sitename}'s features and to start using confirm_password: "Confirm password" action_signup: "Sign up now" welcome_to_ofn: "Welcome to the Open Food Network!" - signup_or_login: "Start By signing up (or logging in)" + signup_or_login: "Start By Signing Up (or logging in)" have_an_account: "Already have an account?" action_login: "Log in now." - forgot_password: "Forgot password?" + forgot_password: "Forgot Password?" password_reset_sent: "An email with instructions on resetting your password has been sent!" reset_password: "Reset password" registration_greeting: "Greetings!" who_is_managing_enterprise: "Who is responsible for managing %{enterprise}?" - enterprise_contact: "Primary contact" + enterprise_contact: "Primary Contact" enterprise_contact_required: "You need to enter a primary contact." - enterprise_email: "Email address" - enterprise_email_required: "You need to enter valid email address." + enterprise_email_address: "Email address" enterprise_phone: "Phone number" back: "Back" continue: "Continue" @@ -522,20 +677,20 @@ See the %{link} to find out more about %{sitename}'s features and to start using limit_reached_message: "You have reached the limit!" limit_reached_text: "You have reached the limit for the number of enterprises you are allowed to own on the" limit_reached_action: "Return to the homepage" - select_promo_image: "Step 3. Select promo image" + select_promo_image: "Step 3. Select Promo Image" promo_image_tip: "Tip: Shown as a banner, preferred size is 1200×260px" promo_image_label: "Choose a promo image" action_or: "OR" promo_image_drag: "Drag and drop your promo here" - review_promo_image: "Step 4. Review your promo banner" + review_promo_image: "Step 4. Review Your Promo Banner" review_promo_image_tip: "Tip: for best results, your promo image should fill the available space" promo_image_placeholder: "Your logo will appear here for review once uploaded" uploading: "Uploading..." - select_logo: "Step 1. Select logo image" + select_logo: "Step 1. Select Logo Image" logo_tip: "Tip: Square images will work best, preferably at least 300×300px" logo_label: "Choose a logo image" logo_drag: "Drag and drop your logo here" - review_logo: "Step 2. Review your logo" + review_logo: "Step 2. Review Your Logo" review_logo_tip: "Tip: for best results, your logo should fill the available space" logo_placeholder: "Your logo will appear here for review once uploaded" enterprise_about_headline: "Nice one!" @@ -592,14 +747,14 @@ Please follow the instructions there to make your enterprise visible on the Open registration_type_error: "Please choose one. Are you are producer?" registration_type_producer_help: "Producers make yummy things to eat and/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it." registration_type_no_producer_help: "If you’re not a producer, you’re probably someone who sells and distributes food. You might be a hub, coop, buying group, retailer, wholesaler or other." - create_profile: "Create profile" + create_profile: "Create Profile" registration_images_headline: "Thanks!" registration_images_description: "Let's upload some pretty pictures so your profile looks great! :)" - registration_detail_headline: "Let's get started" + registration_detail_headline: "Let's Get Started" registration_detail_enterprise: "Woot! First we need to know a little bit about your enterprise:" registration_detail_producer: "Woot! First we need to know a little bit about your farm:" - registration_detail_name_enterprise: "Enterprise name:" - registration_detail_name_producer: "Farm name:" + registration_detail_name_enterprise: "Enterprise Name:" + registration_detail_name_producer: "Farm Name:" registration_detail_name_placeholder: "e.g. Charlie's Awesome Farm" registration_detail_name_error: "Please choose a unique name for your enterprise" registration_detail_address1: "Address line 1:" @@ -635,6 +790,253 @@ Please follow the instructions there to make your enterprise visible on the Open price_graph: "Price graph" included_tax: "Included tax" remove_tax: "Remove tax" + balance: "Balance" + transaction: "Transaction" + transaction_date: "Date" #Transaction is only in key to avoid conflict with :date + payment_state: "Payment status" + shipping_state: "Shipping status" + value: "Value" + balance_due: "Balance due" + credit: "Credit" + Paid: "Paid" + Ready: "Ready" + you_have_no_orders_yet: "You have no orders yet" + running_balance: "Running balance" + outstanding_balance: "Outstanding balance" + admin_entreprise_relationships: "Enterprise Relationships" + admin_entreprise_relationships_everything: "Everything" + admin_entreprise_relationships_permits: "permits" + admin_entreprise_relationships_seach_placeholder: "Search" + admin_entreprise_relationships_button_create: "Create" + admin_entreprise_groups: "Enterprise Groups" + admin_entreprise_groups_name: "Name" + admin_entreprise_groups_owner: "Owner" + admin_entreprise_groups_on_front_page: "On front page ?" + admin_entreprise_groups_entreprise: "Enterprises" + admin_entreprise_groups_primary_details: "Primary Details" + admin_entreprise_groups_data_powertip: "The primary user responsible for this group." + admin_entreprise_groups_data_powertip_logo: "This is the logo for the group" + admin_entreprise_groups_data_powertip_promo_image: "This image is displayed at the top of the Group profile" + admin_entreprise_groups_about: "About" + admin_entreprise_groups_images: "Images" + admin_entreprise_groups_contact: "Contact" + admin_entreprise_groups_contact_phone_placeholder: "eg. 98 7654 3210" + admin_entreprise_groups_contact_address1_placeholder: "eg. 123 High Street" + admin_entreprise_groups_contact_city: "Suburb" + admin_entreprise_groups_contact_city_placeholder: "eg. Northcote" + admin_entreprise_groups_contact_zipcode: "Postcode" + admin_entreprise_groups_contact_zipcode_placeholder: "eg. 3070" + admin_entreprise_groups_contact_state_id: "State" + admin_entreprise_groups_contact_country_id: "Country" + admin_entreprise_groups_web: "Web Resources" + admin_entreprise_groups_web_twitter: "eg. @the_prof" + admin_entreprise_groups_web_website_placeholder: "eg. www.truffles.com" + admin_order_cycles: "Admin Order Cycles" + open: "Open" + close: "Close" + supplier: "Supplier" + coordinator: "Coordinator" + distributor: "Distributor" + product: "Products" + enterprise_fees: "Enterprise Fees" + fee_type: "Fee Type" + tax_category: "Tax Category" + calculator: "Calculator" + calculator_values: "Calculator values" + new_order_cycles: "New Order Cycles" + select_a_coordinator_for_your_order_cycle: "select a coordinator for your order cycle" + edit_order_cycle: "Edit Order Cycle" + roles: "Roles" + update: "Update" + add_producer_property: "Add producer property" + admin_settings: "Settings" + update_invoice: "Update Invoices" + finalise_invoice: "Finalise Invoices" + finalise_user_invoices: "Finalise User Invoices" + finalise_user_invoice_explained: "Use this button to finalize all invoices in the system for the previous calendar month. This task can be set up to run automatically once a month." + manually_run_task: "Manually Run Task " + update_user_invoices: "Update User Invoices" + update_user_invoice_explained: "Use this button to immediately update invoices for the month to date for each enterprise user in the system. This task can be set up to run automatically every night." + auto_finalise_invoices: "Auto-finalise invoices monthly on the 2nd at 1:30am" + auto_update_invoices: "Auto-update invoices nightly at 1:00am" + in_progress: "In Progress" + started_at: "Started at" + queued: "Queued" + scheduled_for: "Scheduled for" + customers: "Customers" + please_select_hub: "Please select a Hub" + loading_customers: "Loading Customers" + no_customers_found: "No customers found" + go: "Go" + hub: "Hub" + accounts_administration_distributor: "accounts administration distributor" + accounts_and_billing: "Accounts & Billing" + producer: "Producer" + product: "Product" + price: "Price" + on_hand: "On hand" + save_changes: "Save Changes" + spree_admin_overview_enterprises_header: "My Enterprises" + spree_admin_overview_enterprises_footer: "MANAGE MY ENTERPRISES" + spree_admin_enterprises_hubs_name: "Name" + spree_admin_enterprises_create_new: "CREATE NEW" + spree_admin_enterprises_shipping_methods: "Shipping Methods" + spree_admin_enterprises_fees: "Enterprise Fees" + spree_admin_enterprises_none_create_a_new_enterprise: "CREATE A NEW ENTERPRISE" + spree_admin_enterprises_none_text: "You don't have any enterprises yet" + spree_admin_enterprises_producers_name: "Name" + spree_admin_enterprises_producers_total_products: "Total Products" + spree_admin_enterprises_producers_active_products: "Active Products" + spree_admin_enterprises_producers_order_cycles: "Products in OCs" + spree_admin_enterprises_producers_order_cycles_title: "" + spree_admin_enterprises_tabs_hubs: "HUBS" + spree_admin_enterprises_tabs_producers: "PRODUCERS" + spree_admin_enterprises_producers_manage_order_cycles: "MANAGE ORDER CYCLES" + spree_admin_enterprises_producers_manage_products: "MANAGE PRODUCTS" + spree_admin_enterprises_producers_orders_cycle_text: "You don't have any active order cycles." + spree_admin_enterprises_any_active_products_text: "You don't have any active products." + spree_admin_enterprises_create_new_product: "CREATE A NEW PRODUCT" + spree_admin_order_cycles: "Order Cycles" + spree_admin_order_cycles_tip: "Order cycles determine when and where your products are available to customers." + dashbord: "Dashboard" + spree_admin_single_enterprise_alert_mail_confirmation: "Please confirm the email address for" + spree_admin_single_enterprise_alert_mail_sent: "We've sent an email to" + spree_admin_overview_action_required: "Action Required" + spree_admin_overview_check_your_inbox: "Please check you inbox for furher instructions. Thanks!" + change_package: "Change Package" + spree_admin_single_enterprise_hint: "Hint: To allow people to find you, turn on your visibility under" + your_profil_live: "Your profile live" + on_ofn_map: "on the Open Food Network map" + see: "See" + live: "live" + manage: "Manage" + resend: "Resend" + add_and_manage_products: "Add & manage products" + add_and_manage_order_cycles: "Add & manage order cycles" + manage_order_cycles: "Manage order cycles" + manage_products: "Manage products" + edit_profile_details: "Edit profile details" + edit_profile_details_etc: "Change your profile description, images, etc." + start_date: "Start Date" + end_date: "End Date" + order_cycle: "Order Cycle" + group_buy_unit_size: "Group Buy Unit Size" + total_qtt_ordered: "Total Quantity Ordered" + max_qtt_ordered: "Max Quantity Ordered" + current_fulfilled_units: "Current Fulfilled Units" + max_fulfilled_units: "Max Fulfilled Units" + bulk_management_warning: "WARNING: Some variants do not have a unit value" + ask: "Ask?" + no_orders_found: "No orders found." + order_no: "Order No." + weight_volume: "Weight/Volume" + remove_tax: "Remove tax" + tax_settings: "Tax Settings" + products_require_tax_category: "products require tax category" + admin_shared_address_1: "Address" + admin_shared_address_2: "Address (cont.)" + admin_share_city: "City" + admin_share_zipcode: "Postcode" + admin_share_country: "Country" + admin_share_state: "State" + hub_sidebar_hubs: "Hubs" + hub_sidebar_none_available: "None Available" + hub_sidebar_manage: "Manage" + hub_sidebar_at_least: "At least one hub must be selected" + hub_sidebar_blue: "blue" + hub_sidebar_red: "red" + shop_trial_in_progress: "Your shopfront trial expires in %{days}." + shop_trial_expired: "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)." #FIXME + report_customers_distributor: "Distributor" + report_customers_supplier: "Supplier" + report_customers_cycle: "Order Cycle" + report_customers_type: "Report Type" + report_customers_csv: "Download as csv" + report_producers: "Producers: " + report_type: "Report Type: " + report_hubs: "Hubs: " + report_payment: "Payment Methods: " + report_distributor: "Distributor: " + report_payment_by: 'Payments By Type' + report_itemised_payment: 'Itemised Payment Totals' + report_payment_totals: 'Payment Totals' + report_all: 'all' + report_order_cycle: "Order Cycle: " + report_entreprises: "Enterprises: " + report_users: "Users: " + initial_invoice_number: "Initial invoice number:" + invoice_date: "Invoice date:" + due_date: "Due date:" + account_code: "Account code:" + equals: "Equals" + contains: "contains" + discount: "Discount" + filter_products: "Filter Products" + delete_product_variant: "The last variant cannot be deleted!" + progress: "progress" + saving: "Saving.." + success: "success" + failure: "failure" + unsaved_changes_confirmation: "Unsaved changes will be lost. Continue anyway?" + one_product_unsaved: "Changes to one product remain unsaved." + products_unsaved: "Changes to %{n} products remain unsaved." + add_manager: "Add a manager" + is_already_manager: "is already a manager!" + no_change_to_save: " No change to save" + add_manager: "Add a manager" + users: "Users" + about: "About" + images: "Images" + contact: "Contact" + web: "Web" + primary_details: "Primary Details" + adrdress: "Address" + contact: "Contact" + social: "Social" + business_details: "Business Details" + properties: "Properties" + shipping_methods: "Shipping Methods" + payment_methods: "Payment Methods" + enterprise_fees: "Enterprise Fees" + inventory_settings: "Inventory Settings" + tag_rules: "Tag Rules" + shop_preferences: "Shop Preferences" + validation_msg_relationship_already_established: "^That relationship is already established." + validation_msg_at_least_one_hub: "^At least one hub must be selected" + validation_msg_product_category_cant_be_blank: "^Product Category cant be blank" + validation_msg_tax_category_cant_be_blank: "^Tax Category can't be blank" + validation_msg_is_associated_with_an_exising_customer: "is associated with an existing customer" + + spree: + shipment_states: + backorder: backorder + partial: partial + pending: pending + ready: ready + shipped: shipped + payment_states: + balance_due: balance due + completed: completed + checkout: checkout + credit_owed: credit owed + failed: failed + paid: paid + pending: pending + processing: processing + void: void + order_state: + address: address + adjustments: adjustments + awaiting_return: awaiting return + canceled: canceled + cart: cart + complete: complete + confirm: confirm + delivery: delivery + payment: payment + resumed: resumed + returned: returned + skrill: skrill shop_trial_length: "Shop Trial Length (Days)" shop_trial_length: "Shop Trial Length (Days)" shop_trial_expires_in: "Your shopfront trial expires in" diff --git a/config/locales/en.yml b/config/locales/en.yml index 47deeda40b..34c4f4d4e9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -15,6 +15,12 @@ # See http://community.openfoodnetwork.org/t/localisation-ofn-in-your-language/397 en: + activerecord: + # Overridden here due to a bug in spree i18n (Issue #870) + attributes: + spree/order: + payment_state: Payment State + shipment_state: Shipment State devise: failure: invalid: | @@ -31,8 +37,8 @@ en: welcome_to: 'Welcome to ' site_meta_description: "We begin from the ground up. With farmers and growers ready to tell their stories proudly and truly. With distributors ready to connect people with products fairly and honestly. With buyers who believe that better weekly shopping decisions can…" search_by_name: Search by name or suburb... - producers: Aussie Producers - producers_join: Australian producers are now welcome to join the Open Food Network. + producers: 'Australian Producers' + producers_join: Australian producers are now welcome to join the Open Food Network. #FIXME charges_sales_tax: Charges GST? print_invoice: "Print Invoice" send_invoice: "Send Invoice" @@ -74,6 +80,10 @@ en: whats_this: What's this? + customers: + index: + add_customer: "Add customer" + customer_placeholder: "customer@example.org" 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 @@ -105,18 +115,44 @@ en: enterprise: select_outgoing_oc_products_from: Select outgoing OC products from + enterprises: + form: + primary_details: + shopfront_requires_login: "Shopfront requires login?" + shopfront_requires_login_tip: "Choose whether customers must login to view the shopfront." + shopfront_requires_login_false: "Public" + shopfront_requires_login_true: "Require customers to login" + + home: + hubs: + show_closed_shops: "Show closed shops" + hide_closed_shops: "Hide closed shops" + show_on_map: "Show all on the map" + shared: + register_call: + selling_on_ofn: "Interested in getting on the Open Food Network?" + register: "Register here" + shop: + messages: + login: "login" + register: "register" + contact: "contact" + require_customer_login: "This shop is for customers only." + require_login_html: "Please %{login} if you have an account already. Otherwise, %{register} to become a customer." + require_customer_html: "Please %{contact} %{enterprise} to become a customer." + # Printable Invoice Columns invoice_column_item: "Item" invoice_column_qty: "Qty" invoice_column_tax: "GST" invoice_column_price: "Price" - logo: "Logo (640x130)" - logo_mobile: "Mobile logo (75x26)" - logo_mobile_svg: "Mobile logo (SVG)" + logo: "Logo (640x130)" #FIXME + logo_mobile: "Mobile logo (75x26)" #FIXME + logo_mobile_svg: "Mobile logo (SVG)" #FIXME home_hero: "Hero image" home_show_stats: "Show statistics" - footer_logo: "Logo (220x76)" + footer_logo: "Logo (220x76)" #FIXME footer_facebook_url: "Facebook URL" footer_twitter_url: "Twitter URL" footer_instagram_url: "Instagram URL" @@ -145,8 +181,6 @@ en: on_demand: On demand none: None - alert_selling_on_ofn: "Interested in selling food on the Open Food Network?" - alert_start_here: "Start here" label_shops: "Shops" label_map: "Map" label_producers: "Producers" @@ -159,7 +193,7 @@ en: label_administration: "Administration" label_admin: "Admin" label_account: "Account" - label_more: "More" + label_more: "Show more" label_less: "Show less" label_notices: "Notices" @@ -286,6 +320,25 @@ en: order_payment_paypal_successful: Your payment via PayPal has been processed successfully. order_hub_info: Hub Info + bom_tip: "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." + bom_shared: "Shared Resource?" + bom_page_title: "Bulk Order Management" + bom_no: "Order no." + bom_date: "Order date" + bom_cycle: "Order cycle" + bom_max: "Max" + bom_hub: "Hub" + bom_variant: "Product: Unit" + bom_final_weigth_volume: "Weight/Volume" + bom_quantity: "Quantity" + bom_actions_delete: "Delete Selected" + bom_loading: "Loading orders" + bom_no_results: "No orders found." + bom_order_error: "Some errors must be resolved before you can update orders.\nAny fields with red borders contain errors." + + unsaved_changes_warning: "Unsaved changes exist and will be lost if you continue." + unsaved_changes_error: "Fields with red borders contain errors." + products: "Products" products_in: "in %{oc}" products_at: "at %{distributor}" @@ -389,6 +442,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using products_cart_empty: "Cart empty" products_edit_cart: "Edit your cart" products_from: from + products_change: "No changes to save." + products_update_error: "Saving failed with the following error(s):" + products_update_error_msg: "Saving failed." + products_update_error_data: "Save failed due to invalid data:" + products_changes_saved: "Changes saved." search_no_results_html: "Sorry, no results found for %{query}. Try another search?" @@ -403,6 +461,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using groups_search: "Search name or keyword" groups_no_groups: "No groups found" groups_about: "About Us" + groups_producers: "Our producers" groups_hubs: "Our hubs" groups_contact_web: Contact @@ -490,13 +549,26 @@ See the %{link} to find out more about %{sitename}'s features and to start using producers_signup_cta_headline: Join now! producers_signup_cta_action: Join now producers_signup_detail: Here's the detail. + producer: Producer products_item: Item products_description: Description products_variant: Variant products_quantity: Quantity products_availabel: Available? - products_price: Price + products_producer: "Producer" + products_price: "Price" + products_sku: "SKU" + products_name: "name" + products_unit: "unit" + products_on_hand: "on hand" + products_on_demand: "On demand?" + products_category: "Category" + products_tax_category: "tax category" + products_available_on: "Available On" + products_inherit: "Inherit?" + products_inherits_properties: "Inherits Properties?" + products_stock_level_reset: "Enable Stock Level Reset?" register_title: Register @@ -542,7 +614,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using products_oc_is: "Your order cycle for this order is %{name}." products_oc_error: "Please complete your order from %{link} before shopping in a different order cycle." products_oc_current: "your current order cycle" - products_quantity: Quantity products_max_quantity: Max quantity products_distributor: Distributor products_distributor_info: When you select a distributor for your order, their address and pickup times will be displayed here. @@ -723,6 +794,253 @@ Please follow the instructions there to make your enterprise visible on the Open price_graph: "Price graph" included_tax: "Included tax" remove_tax: "Remove tax" + balance: "Balance" + transaction: "Transaction" + transaction_date: "Date" #Transaction is only in key to avoid conflict with :date + payment_state: "Payment status" + shipping_state: "Shipping status" + value: "Value" + balance_due: "Balance due" + credit: "Credit" + Paid: "Paid" + Ready: "Ready" + you_have_no_orders_yet: "You have no orders yet" + running_balance: "Running balance" + outstanding_balance: "Outstanding balance" + admin_entreprise_relationships: "Enterprise Relationships" + admin_entreprise_relationships_everything: "Everything" + admin_entreprise_relationships_permits: "permits" + admin_entreprise_relationships_seach_placeholder: "Search" + admin_entreprise_relationships_button_create: "Create" + admin_entreprise_groups: "Enterprise Groups" + admin_entreprise_groups_name: "Name" + admin_entreprise_groups_owner: "Owner" + admin_entreprise_groups_on_front_page: "On front page ?" + admin_entreprise_groups_entreprise: "Enterprises" + admin_entreprise_groups_primary_details: "Primary Details" + admin_entreprise_groups_data_powertip: "The primary user responsible for this group." + admin_entreprise_groups_data_powertip_logo: "This is the logo for the group" + admin_entreprise_groups_data_powertip_promo_image: "This image is displayed at the top of the Group profile" + admin_entreprise_groups_about: "About" + admin_entreprise_groups_images: "Images" + admin_entreprise_groups_contact: "Contact" + admin_entreprise_groups_contact_phone_placeholder: "eg. 98 7654 3210" + admin_entreprise_groups_contact_address1_placeholder: "eg. 123 High Street" + admin_entreprise_groups_contact_city: "Suburb" + admin_entreprise_groups_contact_city_placeholder: "eg. Northcote" + admin_entreprise_groups_contact_zipcode: "Postcode" + admin_entreprise_groups_contact_zipcode_placeholder: "eg. 3070" + admin_entreprise_groups_contact_state_id: "State" + admin_entreprise_groups_contact_country_id: "Country" + admin_entreprise_groups_web: "Web Resources" + admin_entreprise_groups_web_twitter: "eg. @the_prof" + admin_entreprise_groups_web_website_placeholder: "eg. www.truffles.com" + admin_order_cycles: "Admin Order Cycles" + open: "Open" + close: "Close" + supplier: "Supplier" + coordinator: "Coordinator" + distributor: "Distributor" + product: "Products" + enterprise_fees: "Enterprise Fees" + fee_type: "Fee Type" + tax_category: "Tax Category" + calculator: "Calculator" + calculator_values: "Calculator values" + new_order_cycles: "New Order Cycles" + select_a_coordinator_for_your_order_cycle: "select a coordinator for your order cycle" + edit_order_cycle: "Edit Order Cycle" + roles: "Roles" + update: "Update" + add_producer_property: "Add producer property" + admin_settings: "Settings" + update_invoice: "Update Invoices" + finalise_invoice: "Finalise Invoices" + finalise_user_invoices: "Finalise User Invoices" + finalise_user_invoice_explained: "Use this button to finalize all invoices in the system for the previous calendar month. This task can be set up to run automatically once a month." + manually_run_task: "Manually Run Task " + update_user_invoices: "Update User Invoices" + update_user_invoice_explained: "Use this button to immediately update invoices for the month to date for each enterprise user in the system. This task can be set up to run automatically every night." + auto_finalise_invoices: "Auto-finalise invoices monthly on the 2nd at 1:30am" + auto_update_invoices: "Auto-update invoices nightly at 1:00am" + in_progress: "In Progress" + started_at: "Started at" + queued: "Queued" + scheduled_for: "Scheduled for" + customers: "Customers" + please_select_hub: "Please select a Hub" + loading_customers: "Loading Customers" + no_customers_found: "No customers found" + go: "Go" + hub: "Hub" + accounts_administration_distributor: "accounts administration distributor" + accounts_and_billing: "Accounts & Billing" + producer: "Producer" + product: "Product" + price: "Price" + on_hand: "On hand" + save_changes: "Save Changes" + spree_admin_overview_enterprises_header: "My Enterprises" + spree_admin_overview_enterprises_footer: "MANAGE MY ENTERPRISES" + spree_admin_enterprises_hubs_name: "Name" + spree_admin_enterprises_create_new: "CREATE NEW" + spree_admin_enterprises_shipping_methods: "Shipping Methods" + spree_admin_enterprises_fees: "Enterprise Fees" + spree_admin_enterprises_none_create_a_new_enterprise: "CREATE A NEW ENTERPRISE" + spree_admin_enterprises_none_text: "You don't have any enterprises yet" + spree_admin_enterprises_producers_name: "Name" + spree_admin_enterprises_producers_total_products: "Total Products" + spree_admin_enterprises_producers_active_products: "Active Products" + spree_admin_enterprises_producers_order_cycles: "Products in OCs" + spree_admin_enterprises_producers_order_cycles_title: "" + spree_admin_enterprises_tabs_hubs: "HUBS" + spree_admin_enterprises_tabs_producers: "PRODUCERS" + spree_admin_enterprises_producers_manage_order_cycles: "MANAGE ORDER CYCLES" + spree_admin_enterprises_producers_manage_products: "MANAGE PRODUCTS" + spree_admin_enterprises_producers_orders_cycle_text: "You don't have any active order cycles." + spree_admin_enterprises_any_active_products_text: "You don't have any active products." + spree_admin_enterprises_create_new_product: "CREATE A NEW PRODUCT" + spree_admin_order_cycles: "Order Cycles" + spree_admin_order_cycles_tip: "Order cycles determine when and where your products are available to customers." + dashbord: "Dashboard" + spree_admin_single_enterprise_alert_mail_confirmation: "Please confirm the email address for" + spree_admin_single_enterprise_alert_mail_sent: "We've sent an email to" + spree_admin_overview_action_required: "Action Required" + spree_admin_overview_check_your_inbox: "Please check you inbox for furher instructions. Thanks!" + change_package: "Change Package" + spree_admin_single_enterprise_hint: "Hint: To allow people to find you, turn on your visibility under" + your_profil_live: "Your profile live" + on_ofn_map: "on the Open Food Network map" + see: "See" + live: "live" + manage: "Manage" + resend: "Resend" + add_and_manage_products: "Add & manage products" + add_and_manage_order_cycles: "Add & manage order cycles" + manage_order_cycles: "Manage order cycles" + manage_products: "Manage products" + edit_profile_details: "Edit profile details" + edit_profile_details_etc: "Change your profile description, images, etc." + start_date: "Start Date" + end_date: "End Date" + order_cycle: "Order Cycle" + group_buy_unit_size: "Group Buy Unit Size" + total_qtt_ordered: "Total Quantity Ordered" + max_qtt_ordered: "Max Quantity Ordered" + current_fulfilled_units: "Current Fulfilled Units" + max_fulfilled_units: "Max Fulfilled Units" + bulk_management_warning: "WARNING: Some variants do not have a unit value" + ask: "Ask?" + no_orders_found: "No orders found." + order_no: "Order No." + weight_volume: "Weight/Volume" + remove_tax: "Remove tax" + tax_settings: "Tax Settings" + products_require_tax_category: "products require tax category" + admin_shared_address_1: "Address" + admin_shared_address_2: "Address (cont.)" + admin_share_city: "City" + admin_share_zipcode: "Postcode" + admin_share_country: "Country" + admin_share_state: "State" + hub_sidebar_hubs: "Hubs" + hub_sidebar_none_available: "None Available" + hub_sidebar_manage: "Manage" + hub_sidebar_at_least: "At least one hub must be selected" + hub_sidebar_blue: "blue" + hub_sidebar_red: "red" + shop_trial_in_progress: "Your shopfront trial expires in %{days}." + shop_trial_expired: "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)." #FIXME + report_customers_distributor: "Distributor" + report_customers_supplier: "Supplier" + report_customers_cycle: "Order Cycle" + report_customers_type: "Report Type" + report_customers_csv: "Download as csv" + report_producers: "Producers: " + report_type: "Report Type: " + report_hubs: "Hubs: " + report_payment: "Payment Methods: " + report_distributor: "Distributor: " + report_payment_by: 'Payments By Type' + report_itemised_payment: 'Itemised Payment Totals' + report_payment_totals: 'Payment Totals' + report_all: 'all' + report_order_cycle: "Order Cycle: " + report_entreprises: "Enterprises: " + report_users: "Users: " + initial_invoice_number: "Initial invoice number:" + invoice_date: "Invoice date:" + due_date: "Due date:" + account_code: "Account code:" + equals: "Equals" + contains: "contains" + discount: "Discount" + filter_products: "Filter Products" + delete_product_variant: "The last variant cannot be deleted!" + progress: "progress" + saving: "Saving.." + success: "success" + failure: "failure" + unsaved_changes_confirmation: "Unsaved changes will be lost. Continue anyway?" + one_product_unsaved: "Changes to one product remain unsaved." + products_unsaved: "Changes to %{n} products remain unsaved." + add_manager: "Add a manager" + is_already_manager: "is already a manager!" + no_change_to_save: " No change to save" + add_manager: "Add a manager" + users: "Users" + about: "About" + images: "Images" + contact: "Contact" + web: "Web" + primary_details: "Primary Details" + adrdress: "Address" + contact: "Contact" + social: "Social" + business_details: "Business Details" + properties: "Properties" + shipping_methods: "Shipping Methods" + payment_methods: "Payment Methods" + enterprise_fees: "Enterprise Fees" + inventory_settings: "Inventory Settings" + tag_rules: "Tag Rules" + shop_preferences: "Shop Preferences" + validation_msg_relationship_already_established: "^That relationship is already established." + validation_msg_at_least_one_hub: "^At least one hub must be selected" + validation_msg_product_category_cant_be_blank: "^Product Category cant be blank" + validation_msg_tax_category_cant_be_blank: "^Tax Category can't be blank" + validation_msg_is_associated_with_an_exising_customer: "is associated with an existing customer" + + spree: + shipment_states: + backorder: backorder + partial: partial + pending: pending + ready: ready + shipped: shipped + payment_states: + balance_due: balance due + completed: completed + checkout: checkout + credit_owed: credit owed + failed: failed + paid: paid + pending: pending + processing: processing + void: void + order_state: + address: address + adjustments: adjustments + awaiting_return: awaiting return + canceled: canceled + cart: cart + complete: complete + confirm: confirm + delivery: delivery + payment: payment + resumed: resumed + returned: returned + skrill: skrill shop_trial_length: "Shop Trial Length (Days)" shop_trial_expires_in: "Your shopfront trial expires in" shop_trial_expired_notice: "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)." diff --git a/config/routes.rb b/config/routes.rb index 9821a8b4bf..8d8d6d27c3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -87,6 +87,8 @@ Openfoodnetwork::Application.routes.draw do resources :producer_properties do post :update_positions, on: :collection end + + resources :tag_rules, only: [:destroy] end resources :enterprise_relationships @@ -113,7 +115,7 @@ Openfoodnetwork::Application.routes.draw do resources :inventory_items, only: [:create, :update] - resources :customers, only: [:index, :update] + resources :customers, only: [:index, :create, :update, :destroy] resource :content @@ -126,6 +128,8 @@ Openfoodnetwork::Application.routes.draw do resource :business_model_configuration, only: [:edit, :update], controller: 'business_model_configuration' + resource :cache_settings + resource :account, only: [:show], controller: 'account' end @@ -139,6 +143,10 @@ Openfoodnetwork::Application.routes.draw do get :managed, on: :collection get :accessible, on: :collection end + + resource :status do + get :job_queue + end end namespace :open_food_network do diff --git a/config/schedule.rb b/config/schedule.rb index a09ca55dce..8cc8d7d225 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -4,8 +4,15 @@ require 'whenever' env "MAILTO", "rohan@rohanmitchell.com" + # If we use -e with a file containing specs, rspec interprets it and filters out our examples job_type :run_file, "cd :path; :environment_variable=:environment bundle exec script/rails runner :task :output" +job_type :enqueue_job, "cd :path; :environment_variable=:environment bundle exec script/enqueue :task :priority :output" + + +every 1.hour do + rake 'openfoodnetwork:cache:check_products_integrity' +end every 1.day, at: '12:05am' do run_file "lib/open_food_network/integrity_checker.rb" @@ -19,6 +26,10 @@ every 4.hours do rake 'db2fog:backup' end +every 5.minutes do + enqueue_job 'HeartbeatJob', priority: 0 +end + every 1.day, at: '1:00am' do rake 'openfoodnetwork:billing:update_account_invoices' end diff --git a/db/migrate/20160204013914_add_id_to_coordinator_fees.rb b/db/migrate/20160204013914_add_id_to_coordinator_fees.rb new file mode 100644 index 0000000000..74326a6006 --- /dev/null +++ b/db/migrate/20160204013914_add_id_to_coordinator_fees.rb @@ -0,0 +1,5 @@ +class AddIdToCoordinatorFees < ActiveRecord::Migration + def change + add_column :coordinator_fees, :id, :primary_key + end +end diff --git a/db/migrate/20160303004210_create_tag_rules.rb b/db/migrate/20160303004210_create_tag_rules.rb new file mode 100644 index 0000000000..157fee1e69 --- /dev/null +++ b/db/migrate/20160303004210_create_tag_rules.rb @@ -0,0 +1,10 @@ +class CreateTagRules < ActiveRecord::Migration + def change + create_table :tag_rules do |t| + t.references :enterprise, null: false, index: true + t.string :type, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20160316051131_add_require_login_to_enterprise.rb b/db/migrate/20160316051131_add_require_login_to_enterprise.rb new file mode 100644 index 0000000000..68de642b62 --- /dev/null +++ b/db/migrate/20160316051131_add_require_login_to_enterprise.rb @@ -0,0 +1,5 @@ +class AddRequireLoginToEnterprise < ActiveRecord::Migration + def change + add_column :enterprises, :require_login, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20160401043927_change_value_type_of_paypal_passwords.rb b/db/migrate/20160401043927_change_value_type_of_paypal_passwords.rb new file mode 100644 index 0000000000..e03ed25f9e --- /dev/null +++ b/db/migrate/20160401043927_change_value_type_of_paypal_passwords.rb @@ -0,0 +1,15 @@ +class ChangeValueTypeOfPaypalPasswords < ActiveRecord::Migration + def up + Spree::Preference + .where("key like ?", "spree/gateway/pay_pal_express/password/%") + .where(value_type: "string") + .update_all(value_type: "password") + end + + def down + Spree::Preference + .where("key like ?", "spree/gateway/pay_pal_express/password/%") + .where(value_type: "password") + .update_all(value_type: "string") + end +end diff --git a/db/schema.rb b/db/schema.rb index c84337565a..dc54bd8f3d 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 => 20160302044850) do +ActiveRecord::Schema.define(:version => 20160401043927) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -176,7 +176,7 @@ ActiveRecord::Schema.define(:version => 20160302044850) do add_index "cms_snippets", ["site_id", "identifier"], :name => "index_cms_snippets_on_site_id_and_identifier", :unique => true add_index "cms_snippets", ["site_id", "position"], :name => "index_cms_snippets_on_site_id_and_position" - create_table "coordinator_fees", :id => false, :force => true do |t| + create_table "coordinator_fees", :force => true do |t| t.integer "order_cycle_id" t.integer "enterprise_fee_id" end @@ -348,6 +348,7 @@ ActiveRecord::Schema.define(:version => 20160302044850) do t.string "permalink", :null => false t.boolean "charges_sales_tax", :default => false, :null => false t.string "email_address" + t.boolean "require_login", :default => false, :null => false end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" @@ -682,9 +683,9 @@ ActiveRecord::Schema.define(:version => 20160302044850) 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 @@ -1146,6 +1147,13 @@ ActiveRecord::Schema.define(:version => 20160302044850) do t.integer "state_id" end + create_table "tag_rules", :force => true do |t| + t.integer "enterprise_id", :null => false + t.string "type", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "taggings", :force => true do |t| t.integer "tag_id" t.integer "taggable_id" diff --git a/lib/open_food_network/cached_products_renderer.rb b/lib/open_food_network/cached_products_renderer.rb new file mode 100644 index 0000000000..2174bf4094 --- /dev/null +++ b/lib/open_food_network/cached_products_renderer.rb @@ -0,0 +1,47 @@ +require 'open_food_network/products_renderer' + +# Wrapper for ProductsRenderer that caches the JSON output. +# ProductsRenderer::NoProducts is represented in the cache as nil, +# but re-raised to provide the same interface as ProductsRenderer. + +module OpenFoodNetwork + class CachedProductsRenderer + class NoProducts < Exception; end + + def initialize(distributor, order_cycle) + @distributor = distributor + @order_cycle = order_cycle + end + + def products_json + raise NoProducts.new if @distributor.nil? || @order_cycle.nil? + + products_json = Rails.cache.fetch("products-json-#{@distributor.id}-#{@order_cycle.id}") do + log_warning + + begin + uncached_products_json + rescue ProductsRenderer::NoProducts + nil + end + end + + raise NoProducts.new if products_json.nil? + + products_json + end + + + private + + def log_warning + if Rails.env.production? || Rails.env.staging? + Bugsnag.notify RuntimeError.new("Live server MISS on products cache for distributor: #{@distributor.id}, order cycle: #{@order_cycle.id}") + end + end + + def uncached_products_json + ProductsRenderer.new(@distributor, @order_cycle).products_json + end + end +end diff --git a/lib/open_food_network/customers_report.rb b/lib/open_food_network/customers_report.rb index 820bfff7df..d7900a16ac 100644 --- a/lib/open_food_network/customers_report.rb +++ b/lib/open_food_network/customers_report.rb @@ -31,7 +31,7 @@ module OpenFoodNetwork ba.phone, order.distributor.andand.name, [da.andand.address1, da.andand.address2, da.andand.city].join(" "), - order.shipping_method.andand.name + order.shipping_method.andand.name ] end end @@ -78,4 +78,3 @@ module OpenFoodNetwork end end end - diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index ea238f7792..8a9c6ba60f 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -50,7 +50,7 @@ module OpenFoodNetwork order.shipping_method.andand.name, order.payments.first.andand.payment_method.andand.name, order.payments.first.amount, - OpenFoodNetwork::UserBalanceCalculator.new(order.user, order.distributor).balance + OpenFoodNetwork::UserBalanceCalculator.new(order.email, order.distributor).balance ] end @@ -67,23 +67,23 @@ module OpenFoodNetwork order.shipping_method.andand.name, order.payments.first.andand.payment_method.andand.name, order.payments.first.amount, - OpenFoodNetwork::UserBalanceCalculator.new(order.user, order.distributor).balance, + OpenFoodNetwork::UserBalanceCalculator.new(order.email, order.distributor).balance, has_temperature_controlled_items?(order), order.special_instructions ] end def filter_to_payment_method(orders) - if params[:payment_method_name].present? - orders.with_payment_method_name(params[:payment_method_name]) + if params[:payment_method_in].present? + orders.joins(payments: :payment_method).where(spree_payments: { payment_method_id: params[:payment_method_in]}) else orders end end def filter_to_shipping_method(orders) - if params[:shipping_method_name].present? - orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:shipping_method_name]) + if params[:shipping_method_in].present? + orders.joins(:shipping_method).where(shipping_method_id: params[:shipping_method_in]) else orders end diff --git a/lib/open_food_network/products_cache.rb b/lib/open_food_network/products_cache.rb new file mode 100644 index 0000000000..f6ef15829f --- /dev/null +++ b/lib/open_food_network/products_cache.rb @@ -0,0 +1,182 @@ +require 'open_food_network/products_cache_refreshment' + +# When elements of the data model change, refresh the appropriate parts of the products cache. + +module OpenFoodNetwork + class ProductsCache + def self.variant_changed(variant) + exchanges_featuring_variants(variant).each do |exchange| + refresh_cache exchange.receiver, exchange.order_cycle + end + end + + + def self.variant_destroyed(variant, &block) + exchanges = exchanges_featuring_variants(variant).to_a + + block.call + + exchanges.each do |exchange| + refresh_cache exchange.receiver, exchange.order_cycle + end + end + + + def self.product_changed(product) + exchanges_featuring_variants(product.variants).each do |exchange| + refresh_cache exchange.receiver, exchange.order_cycle + end + end + + + def self.variant_override_changed(variant_override) + exchanges_featuring_variants(variant_override.variant, distributor: variant_override.hub).each do |exchange| + refresh_cache exchange.receiver, exchange.order_cycle + end + end + + + def self.variant_override_destroyed(variant_override) + variant_override_changed variant_override + end + + + def self.producer_property_changed(producer_property) + products = producer_property.producer.supplied_products + variants = Spree::Variant. + where(is_master: false, deleted_at: nil). + where(product_id: products) + + exchanges_featuring_variants(variants).each do |exchange| + refresh_cache exchange.receiver, exchange.order_cycle + end + end + + + def self.producer_property_destroyed(producer_property) + producer_property_changed producer_property + end + + + def self.order_cycle_changed(order_cycle) + if order_cycle.dated? && !order_cycle.closed? + order_cycle.exchanges.outgoing.each do |exchange| + refresh_cache exchange.receiver, order_cycle + end + end + end + + + def self.exchange_changed(exchange) + if exchange.incoming + refresh_incoming_exchanges(Exchange.where(id: exchange)) + else + refresh_outgoing_exchange(exchange) + end + end + + + def self.exchange_destroyed(exchange) + exchange_changed exchange + end + + + def self.enterprise_fee_changed(enterprise_fee) + refresh_supplier_fee enterprise_fee + refresh_coordinator_fee enterprise_fee + refresh_distributor_fee enterprise_fee + end + + + def self.distributor_changed(enterprise) + Exchange.cachable.where(receiver_id: enterprise).each do |exchange| + refresh_cache exchange.receiver, exchange.order_cycle + end + end + + + def self.inventory_item_changed(inventory_item) + exchanges_featuring_variants(inventory_item.variant, distributor: inventory_item.enterprise).each do |exchange| + refresh_cache exchange.receiver, exchange.order_cycle + end + end + + + private + + def self.exchanges_featuring_variants(variants, distributor: nil) + exchanges = Exchange. + outgoing. + with_any_variant(variants). + joins(:order_cycle). + merge(OrderCycle.dated). + merge(OrderCycle.not_closed) + + exchanges = exchanges.to_enterprise(distributor) if distributor + + exchanges + end + + + def self.refresh_incoming_exchanges(exchanges) + incoming_exchanges(exchanges).map do |exchange| + outgoing_exchanges_with_variants(exchange.order_cycle, exchange.variant_ids) + end.flatten.uniq.each do |exchange| + refresh_cache exchange.receiver, exchange.order_cycle + end + end + + + def self.refresh_outgoing_exchange(exchange) + if exchange.order_cycle.dated? && !exchange.order_cycle.closed? + refresh_cache exchange.receiver, exchange.order_cycle + end + end + + + def self.refresh_supplier_fee(enterprise_fee) + refresh_incoming_exchanges(enterprise_fee.exchanges) + end + + + def self.refresh_coordinator_fee(enterprise_fee) + enterprise_fee.order_cycles.each do |order_cycle| + order_cycle_changed order_cycle + end + end + + + def self.refresh_distributor_fee(enterprise_fee) + enterprise_fee.exchange_fees. + joins(:exchange => :order_cycle). + merge(Exchange.outgoing). + merge(OrderCycle.dated). + merge(OrderCycle.not_closed). + each do |exf| + + refresh_cache exf.exchange.receiver, exf.exchange.order_cycle + end + end + + + def self.incoming_exchanges(exchanges) + exchanges. + incoming. + joins(:order_cycle). + merge(OrderCycle.dated). + merge(OrderCycle.not_closed) + end + + + def self.outgoing_exchanges_with_variants(order_cycle, variant_ids) + order_cycle.exchanges.outgoing. + joins(:exchange_variants). + where('exchange_variants.variant_id IN (?)', variant_ids) + end + + + def self.refresh_cache(distributor, order_cycle) + ProductsCacheRefreshment.refresh distributor, order_cycle + end + end +end diff --git a/lib/open_food_network/products_cache_integrity_checker.rb b/lib/open_food_network/products_cache_integrity_checker.rb new file mode 100644 index 0000000000..8098124feb --- /dev/null +++ b/lib/open_food_network/products_cache_integrity_checker.rb @@ -0,0 +1,35 @@ +require 'open_food_network/products_renderer' + +module OpenFoodNetwork + class ProductsCacheIntegrityChecker + def initialize(distributor, order_cycle) + @distributor = distributor + @order_cycle = order_cycle + end + + def ok? + diff.none? + end + + def diff + @diff ||= Diffy::Diff.new pretty(cached_json), pretty(rendered_json) + end + + + private + + def cached_json + Rails.cache.read("products-json-#{@distributor.id}-#{@order_cycle.id}") || {}.to_json + end + + def rendered_json + OpenFoodNetwork::ProductsRenderer.new(@distributor, @order_cycle).products_json + rescue OpenFoodNetwork::ProductsRenderer::NoProducts + nil + end + + def pretty(json) + JSON.pretty_generate JSON.parse json + end + end +end diff --git a/lib/open_food_network/products_cache_refreshment.rb b/lib/open_food_network/products_cache_refreshment.rb new file mode 100644 index 0000000000..276734570f --- /dev/null +++ b/lib/open_food_network/products_cache_refreshment.rb @@ -0,0 +1,47 @@ +# When enqueuing a job to refresh the products cache for a particular distribution, there +# is no benefit in having more than one job waiting in the queue to be run. + +# Imagine that an admin updates a product. This calls for the products cache to be +# updated, otherwise customers will see stale data. + +# Now while that update is running, the admin makes another change to the product. Since this change +# has been made after the previous update started running, the already-running update will not +# include that change - we need another job. So we enqueue another one. + +# Before that job starts running, our zealous admin makes yet another change. This time, there +# is a job running *and* there is a job that has not yet started to run. In this case, there's no +# benefit in enqueuing another job. When the previously enqueued job starts running, it will pick up +# our admin's update and include it. So we ignore this change (from a cache refreshment perspective) +# and go home happy to have saved our job worker's time. + +module OpenFoodNetwork + class ProductsCacheRefreshment + def self.refresh(distributor, order_cycle) + unless pending_job? distributor, order_cycle + enqueue_job distributor, order_cycle + end + end + + + private + + def self.pending_job?(distributor, order_cycle) + # To inspect each job, we need to deserialize the payload. + # This is slow, and if it's a problem in practice, we could pre-filter in SQL + # for handlers matching the class name, distributor id and order cycle id. + + Delayed::Job. + where(locked_at: nil). + map(&:payload_object). + select { |j| + j.class == RefreshProductsCacheJob && + j.distributor_id == distributor.id && + j.order_cycle_id == order_cycle.id + }.any? + end + + def self.enqueue_job(distributor, order_cycle) + Delayed::Job.enqueue RefreshProductsCacheJob.new(distributor.id, order_cycle.id), priority: 10 + end + end +end diff --git a/lib/open_food_network/products_renderer.rb b/lib/open_food_network/products_renderer.rb index d745b1b794..6dd0d5f5ed 100644 --- a/lib/open_food_network/products_renderer.rb +++ b/lib/open_food_network/products_renderer.rb @@ -9,8 +9,8 @@ module OpenFoodNetwork @order_cycle = order_cycle end - def products - products = products_for_shop + def products_json + products = load_products if products enterprise_fee_calculator = EnterpriseFeeCalculator.new @distributor, @order_cycle @@ -31,7 +31,7 @@ module OpenFoodNetwork private - def products_for_shop + def load_products if @order_cycle scoper = ScopeProductToHub.new(@distributor) diff --git a/lib/open_food_network/user_balance_calculator.rb b/lib/open_food_network/user_balance_calculator.rb index 73926c1d03..32cd00c90c 100644 --- a/lib/open_food_network/user_balance_calculator.rb +++ b/lib/open_food_network/user_balance_calculator.rb @@ -1,32 +1,30 @@ module OpenFoodNetwork class UserBalanceCalculator - def initialize(user, distributor) - @user = user + def initialize(email, distributor) + @email = email @distributor = distributor end def balance - payment_total - order_total + payment_total - completed_order_total end - private - def order_total - orders.sum &:total + def completed_order_total + completed_orders.sum &:total end def payment_total payments.sum &:amount end - - def orders - Spree::Order.where(distributor_id: @distributor, user_id: @user) + def completed_orders + Spree::Order.where(distributor_id: @distributor, email: @email).complete.not_state(:canceled) end def payments - Spree::Payment.where(order_id: orders) + Spree::Payment.where(order_id: completed_orders, state: "completed") end end end diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake new file mode 100644 index 0000000000..64a5d3909a --- /dev/null +++ b/lib/tasks/cache.rake @@ -0,0 +1,20 @@ +require 'open_food_network/products_cache_integrity_checker' + +namespace :openfoodnetwork do + namespace :cache do + desc 'check the integrity of the products cache' + task :check_products_integrity => :environment do + Exchange.cachable.each do |exchange| + Delayed::Job.enqueue ProductsCacheIntegrityCheckerJob.new(exchange.receiver_id, exchange.order_cycle_id), priority: 20 + end + end + + + desc 'warm the products cache' + task :warm_products => :environment do + Exchange.cachable.each do |exchange| + Delayed::Job.enqueue RefreshProductsCacheJob.new(exchange.receiver_id, exchange.order_cycle_id), priority: 10 + end + end + end +end diff --git a/script/enqueue b/script/enqueue new file mode 100755 index 0000000000..2071414e4e --- /dev/null +++ b/script/enqueue @@ -0,0 +1,61 @@ +#!/usr/bin/env ruby + +# Push a job onto the Delayed Job queue without booting the Rails stack +# Perfect for calling via cron. +# +# Use like this: +# +# ./script/enqueue +# ./script/enqueue Background::ImportJobs + +require 'erb' +require 'yaml' + +ENV["RAILS_ENV"] ||= "development" + +DATABASE_CONFIG = File.expand_path("../../config/database.yml", __FILE__) + +def psql + raise "Missing database.yml" unless File.exists?(DATABASE_CONFIG) + + file = File.read(DATABASE_CONFIG) + erb = ERB.new(file).result + env = ENV["RAILS_ENV"] + config = YAML.load(erb)[env] + + raise "Missing config for #{env} environment" unless config + + "psql".tap do |s| + s << " --host #{config['host']}" if config['host'] + s << " --user #{config['username']}" if config['username'] + s << " --port #{config['port']}" if config['port'] + s << " #{config['database']}" + end +end + +def enqueue_delayed_job(handler, priority=nil) + time = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S") + priority ||= 50 + + sql = <<-SQL + INSERT INTO delayed_jobs ( + handler, + created_at, + updated_at, + run_at, + priority + ) VALUES ( + '--- !ruby/object:#{handler} {}\n', + '#{time}', + '#{time}', + '#{time}', + #{priority} + ); + SQL + + IO.popen(psql, "w") do |io| + io.write sql + end +end + +enqueue_delayed_job ARGV[0], ARGV[1] diff --git a/spec/controllers/admin/customers_controller_spec.rb b/spec/controllers/admin/customers_controller_spec.rb index bb2e4888c2..f64a8057e8 100644 --- a/spec/controllers/admin/customers_controller_spec.rb +++ b/spec/controllers/admin/customers_controller_spec.rb @@ -94,4 +94,45 @@ describe Admin::CustomersController, type: :controller do end end end + + describe "create" do + let(:enterprise) { create(:distributor_enterprise) } + let(:another_enterprise) { create(:distributor_enterprise) } + + def create_customer(enterprise) + spree_put :create, format: :json, customer: { email: 'new@example.com', enterprise_id: enterprise.id } + end + + context "json" do + context "where I manage the customer's enterprise" do + before do + controller.stub spree_current_user: enterprise.owner + end + + it "allows me to create the customer" do + expect { create_customer enterprise }.to change(Customer, :count).by(1) + end + end + + context "where I don't manage the customer's enterprise" do + before do + controller.stub spree_current_user: another_enterprise.owner + end + + it "prevents me from creating the customer" do + expect { create_customer enterprise }.to change(Customer, :count).by(0) + end + end + + context "where I am the admin user" do + before do + controller.stub spree_current_user: create(:admin_user) + end + + it "allows admins to create the customer" do + expect { create_customer enterprise }.to change(Customer, :count).by(1) + end + end + end + end end diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 8b972a3321..05498010bb 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -181,6 +181,58 @@ module Admin end end end + + describe "tag rules" do + let(:enterprise) { create(:distributor_enterprise) } + let!(:tag_rule) { create(:tag_rule, enterprise: enterprise) } + + before do + login_as_enterprise_user [enterprise] + end + + context "discount order rules" do + it "updates the existing rule with new attributes" do + spree_put :update, { + id: enterprise, + enterprise: { + tag_rules_attributes: { + '0' => { + id: tag_rule, + type: "TagRule::DiscountOrder", + preferred_customer_tags: "some,new,tags", + calculator_type: "Spree::Calculator::FlatPercentItemTotal", + calculator_attributes: { id: tag_rule.calculator.id, preferred_flat_percent: "15" } + } + } + } + } + tag_rule.reload + expect(tag_rule.preferred_customer_tags).to eq "some,new,tags" + expect(tag_rule.calculator.preferred_flat_percent).to eq 15 + end + + it "creates new rules with new attributes" do + spree_put :update, { + id: enterprise, + enterprise: { + tag_rules_attributes: { + '0' => { + id: "", + type: "TagRule::DiscountOrder", + preferred_customer_tags: "tags,are,awesome", + calculator_type: "Spree::Calculator::FlatPercentItemTotal", + calculator_attributes: { id: "", preferred_flat_percent: "24" } + } + } + } + } + expect(tag_rule.reload).to be + new_tag_rule = TagRule::DiscountOrder.last + expect(new_tag_rule.preferred_customer_tags).to eq "tags,are,awesome" + expect(new_tag_rule.calculator.preferred_flat_percent).to eq 24 + end + end + end end context "as owner" do diff --git a/spec/controllers/admin/tag_rules_controller_spec.rb b/spec/controllers/admin/tag_rules_controller_spec.rb new file mode 100644 index 0000000000..fa95650479 --- /dev/null +++ b/spec/controllers/admin/tag_rules_controller_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Admin::TagRulesController, type: :controller do + + describe "destroy" do + context "json" do + let(:format) { :json } + + let(:enterprise) { create(:distributor_enterprise) } + let!(:tag_rule) { create(:tag_rule, enterprise: enterprise) } + let(:params) { { format: format, id: tag_rule.id } } + + context "where I don't manage the tag rule enterprise" do + let(:user) { create(:user) } + + before do + user.owned_enterprises << create(:enterprise) + allow(controller).to receive(:spree_current_user) { user } + end + + it "redirects to unauthorized" do + spree_delete :destroy, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "where I manage the tag rule enterprise" do + before do + allow(controller).to receive(:spree_current_user) { enterprise.owner } + end + + it { expect{ spree_delete :destroy, params }.to change{TagRule.count}.by(-1) } + end + end + end +end diff --git a/spec/controllers/api/statuses_controller_spec.rb b/spec/controllers/api/statuses_controller_spec.rb new file mode 100644 index 0000000000..f2427efb79 --- /dev/null +++ b/spec/controllers/api/statuses_controller_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +module Api + describe StatusesController do + render_views + + describe "job queue status" do + it "returns alive when up to date" do + Spree::Config.last_job_queue_heartbeat_at = Time.now + spree_get :job_queue + response.should be_success + response.body.should == {alive: true}.to_json + end + + it "returns dead otherwise" do + Spree::Config.last_job_queue_heartbeat_at = 10.minutes.ago + spree_get :job_queue + response.should be_success + response.body.should == {alive: false}.to_json + end + + it "returns dead when no heartbeat recorded" do + Spree::Config.last_job_queue_heartbeat_at = nil + spree_get :job_queue + response.should be_success + response.body.should == {alive: false}.to_json + end + end + end +end diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 09ea85dd44..73231bc86c 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe ShopController do - let(:d) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise) } it "redirects to the home page if no distributor is selected" do spree_get :show @@ -11,26 +11,26 @@ describe ShopController do describe "with a distributor in place" do before do - controller.stub(:current_distributor).and_return d + controller.stub(:current_distributor).and_return distributor end - describe "Selecting order cycles" do + describe "selecting an order cycle" do it "should select an order cycle when only one order cycle is open" do - oc1 = create(:simple_order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [distributor]) spree_get :show controller.current_order_cycle.should == oc1 end it "should not set an order cycle when multiple order cycles are open" do - oc1 = create(:simple_order_cycle, distributors: [d]) - oc2 = create(:simple_order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [distributor]) + oc2 = create(:simple_order_cycle, distributors: [distributor]) spree_get :show - controller.current_order_cycle.should == nil + controller.current_order_cycle.should be_nil end it "should allow the user to post to select the current order cycle" do - oc1 = create(:simple_order_cycle, distributors: [d]) - oc2 = create(:simple_order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [distributor]) + oc2 = create(:simple_order_cycle, distributors: [distributor]) spree_post :order_cycle, order_cycle_id: oc2.id response.should be_success @@ -39,9 +39,10 @@ describe ShopController 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]) - oc2 = create(:simple_order_cycle, distributors: [d]) + + it "should return the order cycle details when the OC is selected" do + oc1 = create(:simple_order_cycle, distributors: [distributor]) + oc2 = create(:simple_order_cycle, distributors: [distributor]) spree_post :order_cycle, order_cycle_id: oc2.id response.should be_success @@ -49,7 +50,7 @@ describe ShopController do end it "should return the current order cycle when hit with GET" do - oc1 = create(:simple_order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [distributor]) controller.stub(:current_order_cycle).and_return oc1 spree_get :order_cycle response.body.should have_content oc1.id @@ -57,13 +58,13 @@ describe ShopController do end it "should not allow the user to select an invalid order cycle" do - oc1 = create(:simple_order_cycle, distributors: [d]) - oc2 = create(:simple_order_cycle, distributors: [d]) + oc1 = create(:simple_order_cycle, distributors: [distributor]) + oc2 = create(:simple_order_cycle, distributors: [distributor]) oc3 = create(:simple_order_cycle, distributors: [create(:distributor_enterprise)]) spree_post :order_cycle, order_cycle_id: oc3.id response.status.should == 404 - controller.current_order_cycle.should == nil + controller.current_order_cycle.should be_nil end end @@ -71,31 +72,32 @@ describe ShopController do describe "producers/suppliers" do let(:supplier) { create(:supplier_enterprise) } let(:product) { create(:product, supplier: supplier) } - let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator: create(:distributor_enterprise)) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } before do - exchange = Exchange.find(order_cycle.exchanges.to_enterprises(d).outgoing.first.id) + exchange = order_cycle.exchanges.to_enterprises(distributor).outgoing.first exchange.variants << product.master end end describe "returning products" do - 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(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } + let(:exchange) { order_cycle.exchanges.to_enterprises(distributor).outgoing.first } describe "requests and responses" do let(:product) { create(:product) } + before do exchange.variants << product.variants.first end - it "returns products via json" do + it "returns products via JSON" do controller.stub(:current_order_cycle).and_return order_cycle xhr :get, :products response.should be_success end - it "does not return products if no order_cycle is selected" do + it "does not return products if no order cycle is selected" do controller.stub(:current_order_cycle).and_return nil xhr :get, :products response.status.should == 404 diff --git a/spec/factories.rb b/spec/factories.rb index 21c6cecd15..b4a3a1b573 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -60,8 +60,8 @@ FactoryGirl.define do factory :simple_order_cycle, :class => OrderCycle do sequence(:name) { |n| "Order Cycle #{n}" } - orders_open_at { Time.zone.now - 1.day } - orders_close_at { Time.zone.now + 1.week } + orders_open_at { 1.day.ago } + orders_close_at { 1.week.from_now } coordinator { Enterprise.is_distributor.first || FactoryGirl.create(:distributor_enterprise) } @@ -84,6 +84,26 @@ FactoryGirl.define do end end + factory :undated_order_cycle, parent: :simple_order_cycle do + orders_open_at nil + orders_close_at nil + end + + factory :upcoming_order_cycle, parent: :simple_order_cycle do + orders_open_at { 1.week.from_now } + orders_close_at { 2.weeks.from_now } + end + + factory :open_order_cycle, parent: :simple_order_cycle do + orders_open_at { 1.week.ago } + orders_close_at { 1.week.from_now } + end + + factory :closed_order_cycle, parent: :simple_order_cycle do + orders_open_at { 2.weeks.ago } + orders_close_at { 1.week.ago } + end + factory :exchange, :class => Exchange do order_cycle { OrderCycle.first || FactoryGirl.create(:simple_order_cycle) } sender { FactoryGirl.create(:enterprise) } @@ -197,6 +217,26 @@ FactoryGirl.define do distributor { create(:distributor_enterprise) } end + factory :order_with_credit_payment, parent: :completed_order_with_totals do + distributor { create(:distributor_enterprise)} + order_cycle { create(:simple_order_cycle) } + + after(:create) do |order| + create(:payment, amount: order.total + 10000, order: order, state: "completed") + order.reload + end + end + + factory :order_without_full_payment, parent: :completed_order_with_totals do + distributor { create(:distributor_enterprise)} + order_cycle { create(:simple_order_cycle) } + + after(:create) do |order| + create(:payment, amount: order.total - 1, order: order, state: "completed") + order.reload + end + end + factory :zone_with_member, :parent => :zone do default_tax true @@ -245,6 +285,17 @@ FactoryGirl.define do year { 2000 + rand(100) } month { 1 + rand(12) } end + + factory :filter_shipping_methods_tag_rule, class: TagRule::FilterShippingMethods do + enterprise { FactoryGirl.create :distributor_enterprise } + end + + factory :tag_rule, class: TagRule::DiscountOrder do + enterprise { FactoryGirl.create :distributor_enterprise } + before(:create) do |tr| + tr.calculator = Spree::Calculator::FlatPercentItemTotal.new(calculable: tr) + end + end end diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 767274b274..0b23e77f6c 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 + # hide dropdown first("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "price", with: "$50.00" diff --git a/spec/features/admin/caching_spec.rb b/spec/features/admin/caching_spec.rb new file mode 100644 index 0000000000..22d372e19b --- /dev/null +++ b/spec/features/admin/caching_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require 'open_food_network/products_renderer' + +feature 'Caching' do + include AuthenticationWorkflow + include WebHelper + + before { quick_login_as_admin } + + describe "displaying integrity checker results" do + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:open_order_cycle, distributors: [distributor]) } + + it "displays results when things are good" do + # Given matching data + Rails.cache.write "products-json-#{distributor.id}-#{order_cycle.id}", "[1, 2, 3]\n" + OpenFoodNetwork::ProductsRenderer.stub(:new) { double(:pr, products_json: "[1, 2, 3]\n") } + + # When I visit the cache status page + visit spree.admin_path + click_link 'Configuration' + click_link 'Caching' + + # Then I should see some status information + page.should have_content "OK" + end + + it "displays results when there are errors" do + # Given matching data + Rails.cache.write "products-json-#{distributor.id}-#{order_cycle.id}", "[1, 2, 3]\n" + OpenFoodNetwork::ProductsRenderer.stub(:new) { double(:pr, products_json: "[1, 3]\n") } + + # When I visit the cache status page + visit spree.admin_path + click_link 'Configuration' + click_link 'Caching' + + # Then I should see some status information + page.should have_content "Error" + end + + end +end diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 46a0105bb4..3579421b84 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -82,6 +82,10 @@ feature %q{ page.should have_selector '.available' choose 'Own' + # Require login to view shopfront + expect(page).to have_checked_field "enterprise_require_login_false" + choose "Require customers to login" + within (".side_menu") { click_link "Users" } select2_search user.email, from: 'Owner' @@ -162,6 +166,8 @@ feature %q{ page.should have_field 'enterprise_name', :with => 'Eaterprises' @enterprise.reload expect(@enterprise.owner).to eq user + expect(page).to have_checked_field "enterprise_visible_true" + expect(page).to have_checked_field "enterprise_require_login_true" click_link "Business Details" page.should have_checked_field "enterprise_charges_sales_tax_true" @@ -251,6 +257,31 @@ feature %q{ end + describe "inventory settings", js: true do + let!(:enterprise) { create(:distributor_enterprise) } + let!(:product) { create(:simple_product) } + let!(:order_cycle) { create(:simple_order_cycle, distributors: [enterprise], variants: [product.variants.first]) } + + before do + Delayed::Job.destroy_all + quick_login_as_admin + end + + it "refreshes the cache when I change what products appear on my shopfront" do + # Given a product that's not in my inventory, but is in an active order cycle + + # When I change which products appear on the shopfront + visit edit_admin_enterprise_path(enterprise) + within(".side_menu") { click_link 'Inventory Settings' } + choose 'enterprise_preferred_product_selection_from_inventory_only_1' + + # Then a job should have been enqueued to refresh the cache + expect do + click_button 'Update' + end.to enqueue_job RefreshProductsCacheJob, distributor_id: enterprise.id, order_cycle_id: order_cycle.id + end + end + context "as an Enterprise user", js: true do let(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } let(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index 5c16f886e5..abb5feaf3e 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -42,14 +42,33 @@ feature %q{ check "payment_method_distributor_ids_#{@distributors[1].id}" check "payment_method_distributor_ids_#{@distributors[2].id}" select2_select "PayPal Express", from: "payment_method_type" + expect(page).to have_field 'Login' + fill_in 'payment_method_preferred_login', with: 'testlogin' + fill_in 'payment_method_preferred_password', with: 'secret' + fill_in 'payment_method_preferred_signature', with: 'sig' + click_button 'Update' - flash_message.should eq 'Payment Method has been successfully updated!' + expect(flash_message).to eq 'Payment Method has been successfully updated!' payment_method = Spree::PaymentMethod.find_by_name('New PM Name') expect(payment_method.distributors).to include @distributors[1], @distributors[2] expect(payment_method.distributors).not_to include @distributors[0] expect(payment_method.type).to eq "Spree::Gateway::PayPalExpress" + expect(payment_method.preferences[:login]).to eq 'testlogin' + expect(payment_method.preferences[:password]).to eq 'secret' + expect(payment_method.preferences[:signature]).to eq 'sig' + + fill_in 'payment_method_preferred_login', with: 'otherlogin' + click_button 'Update' + + expect(flash_message).to eq 'Payment Method has been successfully updated!' + expect(page).to have_field 'Password', with: '' + + payment_method = Spree::PaymentMethod.find_by_name('New PM Name') + expect(payment_method.preferences[:login]).to eq 'otherlogin' + expect(payment_method.preferences[:password]).to eq 'secret' + expect(payment_method.preferences[:signature]).to eq 'sig' end end diff --git a/spec/features/admin/shipping_methods_spec.rb b/spec/features/admin/shipping_methods_spec.rb index 9f04b6707b..646b383c21 100644 --- a/spec/features/admin/shipping_methods_spec.rb +++ b/spec/features/admin/shipping_methods_spec.rb @@ -93,12 +93,16 @@ feature 'shipping methods' do fill_in 'shipping_method_name', :with => 'Teleport' check "shipping_method_distributor_ids_#{distributor1.id}" + find(:css, "tags-input .tags input").set "local\n" + click_button 'Create' flash_message.should == 'Shipping method "Teleport" has been successfully created!' + expect(first('tags-input .tag-list ti-tag-item')).to have_content "local" shipping_method = Spree::ShippingMethod.find_by_name('Teleport') shipping_method.distributors.should == [distributor1] + shipping_method.tag_list.should == ["local"] end it "shows me only shipping methods I have access to" do diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb new file mode 100644 index 0000000000..e91215a21d --- /dev/null +++ b/spec/features/admin/tag_rules_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +feature 'Tag Rules', js: true do + include AuthenticationWorkflow + include WebHelper + + let!(:enterprise) { create(:distributor_enterprise) } + + context "creating" do + before do + login_to_admin_section + visit main_app.edit_admin_enterprise_path(enterprise) + end + + it "allows creation of rules of each type" do + click_link "Tag Rules" + + # Creating a new tag + expect(page).to_not have_selector '.customer_tag' + expect(page).to have_content 'No tags apply to this enterprise yet' + click_button '+ Add A New Tag' + find(:css, "tags-input .tags input").set "volunteer\n" + + # New FilterShippingMethods Rule + click_button '+ Add A New Rule' + select2_select 'Show/Hide shipping methods', from: 'rule_type_selector' + click_button "Add Rule" + select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" + + # New DiscountOrder Rule + # expect(page).to have_content 'No rules apply to this tag yet' + # click_button '+ Add A New Rule' + # select2_select 'Apply a discount to orders', from: 'rule_type_selector' + # click_button "Add Rule" + # fill_in "enterprise_tag_rules_attributes_1_calculator_attributes_preferred_flat_percent", with: 22 + + click_button 'Update' + + # tag_rule = TagRule::DiscountOrder.last + # expect(tag_rule.preferred_customer_tags).to eq "volunteer" + # expect(tag_rule.calculator.preferred_flat_percent).to eq -22 + + tag_rule = TagRule::FilterShippingMethods.last + expect(tag_rule.preferred_customer_tags).to eq "volunteer" + expect(tag_rule.preferred_shipping_method_tags).to eq "volunteer" + expect(tag_rule.preferred_matched_shipping_methods_visibility).to eq "hidden" + end + end + + context "updating" do + let!(:do_tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } + let!(:fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "hidden", preferred_customer_tags: "member" ) } + + before do + login_to_admin_section + visit main_app.edit_admin_enterprise_path(enterprise) + end + + it "saves changes to rules of each type" do + click_link "Tag Rules" + + # Tag group exists + expect(first('.customer_tag .header')).to have_content "For customers tagged:" + expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" + find(:css, "tags-input .tags input").set "volunteer\n" + + # DiscountOrder rule + expect(page).to have_field "enterprise_tag_rules_attributes_0_calculator_attributes_preferred_flat_percent", with: '0' + fill_in "enterprise_tag_rules_attributes_0_calculator_attributes_preferred_flat_percent", with: 45 + + # FilterShippingMethods rule + expect(page).to have_select2 "enterprise_tag_rules_attributes_1_preferred_matched_shipping_methods_visibility", selected: 'NOT VISIBLE' + select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_1_preferred_matched_shipping_methods_visibility" + + click_button 'Update' + + # DiscountOrder rule + expect(do_tag_rule.preferred_customer_tags).to eq "member,volunteer" + expect(do_tag_rule.calculator.preferred_flat_percent).to eq -45 + + # FilterShippingMethods rule + expect(fsm_tag_rule.preferred_customer_tags).to eq "member,volunteer" + expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "member,volunteer" + expect(fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "visible" + end + end + + context "deleting" do + let!(:tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } + + before do + login_to_admin_section + visit main_app.edit_admin_enterprise_path(enterprise) + end + + it "deletes rules from the database" do + click_link "Tag Rules" + + expect(page).to have_selector "#tr_#{tag_rule.id}" + + expect{ + within "#tr_#{tag_rule.id}" do + first("a.delete-tag-rule").click + end + expect(page).to_not have_selector "#tr_#{tag_rule.id}" + }.to change{TagRule.count}.by(-1) + end + end +end diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 6f59b78aec..a3a08c2466 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -318,7 +318,7 @@ feature %q{ select2_select hub.name, from: 'hub_id' end - it "alerts the user to the presence of new products, and allows them to be added or hidden" do + it "alerts the user to the presence of new products, and allows them to be added or hidden", retry: 3 do expect(page).to have_no_selector "table#variant-overrides tr#v_#{variant1.id}" expect(page).to have_no_selector "table#variant-overrides tr#v_#{variant2.id}" diff --git a/spec/features/consumer/account_spec.rb b/spec/features/consumer/account_spec.rb new file mode 100644 index 0000000000..b072a24357 --- /dev/null +++ b/spec/features/consumer/account_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +feature %q{ + As a consumer + I want to view my order history with each hub + and view any outstanding balance. +}, js: true do + include UIComponentHelper + include AuthenticationWorkflow + let!(:user) { create(:user)} + let!(:user2) {create(:user)} + let!(:distributor1) { create(:distributor_enterprise) } + let!(:distributor2) { create(:distributor_enterprise) } + let!(:distributor_credit) { create(:distributor_enterprise) } + let!(:distributor_without_orders) { create(:distributor_enterprise) } + let!(:d1o1) { create(:completed_order_with_totals, distributor_id: distributor1.id, user_id: user.id, total: 10000)} + let!(:d1o2) { create(:order_without_full_payment, distributor_id: distributor1.id, user_id: user.id, total: 5000)} + let!(:d2o1) { create(:completed_order_with_totals, distributor_id: distributor2.id, user_id: user.id)} + let!(:credit_order) { create(:order_with_credit_payment, distributor_id: distributor_credit.id, user_id: user.id)} +# let!(:credit_payment) { create(:payment, amount: 12000.00, order_id: credit_order.id)} + + + before do + credit_order.update! + login_as user + visit "/account" + end + + it "shows all hubs that have been ordered from with balance or credit" do + expect(page).to have_content distributor1.name + expect(page).to have_content distributor2.name + expect(page).not_to have_content distributor_without_orders.name + expect(page).to have_content distributor1.name + " " + "Balance due" + expect(page).to have_content distributor_credit.name + " Credit" + end + + it "reveals table of orders for distributors when clicked" do + expand_active_table_node distributor1.name + expect(page).to have_link "Order " + d1o1.number, href:"/orders/#{d1o1.number}" + + expand_active_table_node distributor2.name + expect(page).not_to have_content "Order " + d1o1.number.to_s + end + + context "for a user without orders" do + before do + login_as user2 + visit "/account" + end + + it "displays an appropriate message" do + expect(page).to have_content {t :you_have_no_orders_yet} + end + end + +end diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 1df700a1fc..74145bbe1a 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -28,6 +28,7 @@ feature "As a consumer I want to check out my cart", js: true do describe "with shipping and payment methods" do let(:sm1) { create(:shipping_method, require_ship_address: true, name: "Frogs", description: "yellow", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0.00)) } let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 4.56)) } + let(:sm3) { create(:shipping_method, require_ship_address: false, name: "Local", tag_list: "local") } let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") } let!(:pm2) { create(:payment_method, distributors: [distributor]) } let!(:pm3) do @@ -41,6 +42,7 @@ feature "As a consumer I want to check out my cart", js: true do before do distributor.shipping_methods << sm1 distributor.shipping_methods << sm2 + distributor.shipping_methods << sm3 end context "on the checkout page" do @@ -68,10 +70,11 @@ feature "As a consumer I want to check out my cart", js: true do page.should_not have_content product.tax_category.name end - it "shows all shipping methods, but doesn't show ship address when not needed" do + it "shows all shipping methods" do toggle_shipping page.should have_content "Frogs" page.should have_content "Donkeys" + page.should have_content "Local" end context "when shipping method requires an address" do @@ -84,6 +87,39 @@ feature "As a consumer I want to check out my cart", js: true do find("#ship_address > div.visible").visible?.should be_true end end + + context "using FilterShippingMethods" do + it "shows shipping methods allowed by the rule" do + # No rules in effect + toggle_shipping + page.should have_content "Frogs" + page.should have_content "Donkeys" + page.should have_content "Local" + + create(:filter_shipping_methods_tag_rule, + enterprise: distributor, + preferred_customer_tags: "local", + preferred_shipping_method_tags: "local", + preferred_matched_shipping_methods_visibility: 'visible') + visit checkout_path + checkout_as_guest + + # Rule in effect, disallows access to 'Local' + page.should have_content "Frogs" + page.should have_content "Donkeys" + page.should_not have_content "Local" + + customer = create(:customer, enterprise: distributor, tag_list: "local") + order.update_attribute(:customer_id, customer.id) + visit checkout_path + checkout_as_guest + + # #local Customer can access 'Local' shipping method + page.should have_content "Frogs" + page.should have_content "Donkeys" + page.should have_content "Local" + end + end end context "on the checkout page with payments open" do diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index e81a792881..a8545af3db 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -253,5 +253,78 @@ feature "As a consumer I want to shop with a distributor", js: true do page.should have_content "The next cycle opens in 10 days" end end + + context "when shopping requires a customer" do + let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } + let(:product) { create(:simple_product) } + let(:variant) { create(:variant, product: product) } + + before do + add_product_and_variant_to_order_cycle(exchange, product, variant) + set_order_cycle(order, oc1) + distributor.require_login = true + distributor.save! + end + + context "when not logged in" do + it "tells us to login" do + visit shop_path + expect(page).to have_content "This shop is for customers only." + expect(page).to have_content "Please login" + expect(page).to have_no_content product.name + end + end + + context "when logged in" do + let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } + let(:user) { create(:user, bill_address: address, ship_address: address) } + + before do + quick_login_as user + end + + context "as non-customer" do + it "tells us to contact enterprise" do + visit shop_path + expect(page).to have_content "This shop is for customers only." + expect(page).to have_content "Please contact #{distributor.name}" + expect(page).to have_no_content product.name + end + end + + context "as customer" do + let!(:customer) { create(:customer, user: user, enterprise: distributor) } + + it "shows just products" do + visit shop_path + expect(page).to have_no_content "This shop is for customers only." + expect(page).to have_content product.name + end + end + + context "as a manager" do + let!(:role) { create(:enterprise_role, user: user, enterprise: distributor) } + + it "shows just products" do + visit shop_path + expect(page).to have_no_content "This shop is for customers only." + expect(page).to have_content product.name + end + end + + context "as the owner" do + before do + distributor.owner = user + distributor.save! + end + + it "shows just products" do + visit shop_path + expect(page).to have_no_content "This shop is for customers only." + expect(page).to have_content product.name + end + end + end + end end end diff --git a/spec/features/consumer/shops_spec.rb b/spec/features/consumer/shops_spec.rb index faeff9c37d..4edaa702e0 100644 --- a/spec/features/consumer/shops_spec.rb +++ b/spec/features/consumer/shops_spec.rb @@ -26,9 +26,17 @@ feature 'Shops', js: true do page.should_not have_content invisible_distributor.name end - it "should grey out hubs that are not in an order cycle" do + it "should not show hubs that are not in an order cycle" do create(:simple_product, distributors: [d1, d2]) visit shops_path + page.should have_no_selector 'hub.inactive' + page.should have_no_selector 'hub', text: d2.name + end + + it "should show closed shops after clicking the button" do + create(:simple_product, distributors: [d1, d2]) + visit shops_path + click_link "Show closed shops" page.should have_selector 'hub.inactive' page.should have_selector 'hub.inactive', text: d2.name end diff --git a/spec/helpers/enterprises_helper_spec.rb b/spec/helpers/enterprises_helper_spec.rb new file mode 100644 index 0000000000..1dc32d63b3 --- /dev/null +++ b/spec/helpers/enterprises_helper_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe EnterprisesHelper do + describe "loading available shipping methods" do + + context "when a FilterShippingMethods tag rule is in effect, with preferred visibility of 'visible'" do + let!(:distributor) { create(:distributor_enterprise) } + let!(:allowed_customer) { create(:customer, enterprise: distributor, tag_list: "local") } + let!(:disallowed_customer) { create(:customer, enterprise: distributor, tag_list: "") } + let!(:order) { create(:order, distributor: distributor) } + let!(:tag_rule) { create(:filter_shipping_methods_tag_rule, + enterprise: distributor, + preferred_customer_tags: "local", + preferred_shipping_method_tags: "local-delivery") } + let!(:tagged_sm) { create(:shipping_method, require_ship_address: false, name: "Untagged", tag_list: "local-delivery") } + let!(:untagged_sm) { create(:shipping_method, require_ship_address: false, name: "Tagged", tag_list: "") } + + before do + distributor.shipping_methods = [tagged_sm, untagged_sm] + allow(helper).to receive(:current_order) { order } + end + + context "with a preferred visiblity of 'visible" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'visible') } + + context "when the customer is nil" do + it "applies default action (hide)" do + expect(helper.available_shipping_methods).to include untagged_sm + expect(helper.available_shipping_methods).to_not include tagged_sm + end + end + + context "when the customer's tags match" do + before { order.update_attribute(:customer_id, allowed_customer.id) } + + it "applies the action (show)" do + expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm + end + end + + context "when the customer's tags don't match" do + before { order.update_attribute(:customer_id, disallowed_customer.id) } + + it "applies the default action (hide)" do + expect(helper.available_shipping_methods).to include untagged_sm + expect(helper.available_shipping_methods).to_not include tagged_sm + end + end + end + + context "with a preferred visiblity of 'hidden" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'hidden') } + + context "when the customer is nil" do + it "applies default action (show)" do + expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm + end + end + + context "when the customer's tags match" do + before { order.update_attribute(:customer_id, allowed_customer.id) } + + it "applies the action (hide)" do + expect(helper.available_shipping_methods).to include untagged_sm + expect(helper.available_shipping_methods).to_not include tagged_sm + end + end + + context "when the customer's tags don't match" do + before { order.update_attribute(:customer_id, disallowed_customer.id) } + + it "applies the default action (show)" do + expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm + end + end + end + end + end +end diff --git a/spec/helpers/injection_helper_spec.rb b/spec/helpers/injection_helper_spec.rb index 362ca6479b..3b62fb2660 100644 --- a/spec/helpers/injection_helper_spec.rb +++ b/spec/helpers/injection_helper_spec.rb @@ -3,6 +3,13 @@ require 'spec_helper' describe InjectionHelper do let!(:enterprise) { create(:distributor_enterprise, facebook: "roger") } + let!(:distributor1) { create(:distributor_enterprise) } + let!(:distributor2) { create(:distributor_enterprise) } + let!(:user) { create(:user)} + let!(:d1o1) { create(:completed_order_with_totals, distributor: distributor1, user_id: user.id, total: 10000)} + let!(:d1o2) { create(:completed_order_with_totals, distributor: distributor1, user_id: user.id, total: 5000)} + let!(:d2o1) { create(:completed_order_with_totals, distributor: distributor2, user_id: user.id)} + it "will inject via AMS" do helper.inject_json_ams("test", [enterprise], Api::IdSerializer).should match /#{enterprise.id}/ end @@ -20,7 +27,10 @@ describe InjectionHelper do it "injects shipping_methods" do sm = create(:shipping_method) helper.stub(:current_order).and_return order = create(:order) - helper.stub_chain(:current_distributor, :shipping_methods, :uniq).and_return [sm] + shipping_methods = double(:shipping_methods, uniq: [sm]) + current_distributor = double(:distributor, shipping_methods: shipping_methods) + allow(helper).to receive(:current_distributor) { current_distributor } + allow(current_distributor).to receive(:apply_tag_rules_to).with(shipping_methods, {customer: nil} ) helper.inject_available_shipping_methods.should match sm.id.to_s helper.inject_available_shipping_methods.should match sm.compute_amount(order).to_s end @@ -42,8 +52,4 @@ describe InjectionHelper do helper.inject_taxons.should match taxon.name end - it "injects taxons" do - taxon = create(:taxon) - helper.inject_taxons.should match taxon.name - end end diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index 22777a6528..215f2834ed 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -1,25 +1,49 @@ describe "CustomersCtrl", -> - ctrl = null scope = null - Customers = null + http = null beforeEach -> - shops = "list of shops" - module('admin.customers') - inject ($controller, $rootScope, _Customers_) -> + inject ($controller, $rootScope, _CustomerResource_, $httpBackend) -> scope = $rootScope - Customers = _Customers_ - ctrl = $controller 'customersCtrl', {$scope: scope, Customers: Customers, shops: shops} + http = $httpBackend + $controller 'customersCtrl', {$scope: scope, CustomerResource: _CustomerResource_, shops: {}} + this.addMatchers + toAngularEqual: (expected) -> + return angular.equals(this.actual, expected) + + it "has no shop pre-selected", -> + expect(scope.shop).toEqual {} describe "setting the shop on scope", -> + customer = { id: 5, email: 'someone@email.com'} + customers = [customer] + beforeEach -> - spyOn(Customers, "index").andReturn "list of customers" + http.expectGET('/admin/customers.json?enterprise_id=1').respond 200, customers scope.$apply -> scope.shop = {id: 1} + http.flush() - it "calls Customers#index with the correct params", -> - expect(Customers.index).toHaveBeenCalledWith({enterprise_id: 1}) + it "retrievs the list of customers", -> + expect(scope.customers).toAngularEqual customers - it "resets $scope.customers with the result of Customers#index", -> - expect(scope.customers).toEqual "list of customers" + describe "scope.add", -> + it "creates a new customer", -> + email = "customer@example.org" + newCustomer = {id: 6, email: email} + customers.push(newCustomer) + http.expectPOST('/admin/customers.json?email=' + email + '&enterprise_id=1').respond 200, newCustomer + scope.add(email) + http.flush() + expect(scope.customers).toAngularEqual customers + + describe "scope.deleteCustomer", -> + it "deletes a customer", -> + expect(scope.customers.length).toBe 2 + customer = scope.customers[0] + http.expectDELETE('/admin/customers/' + customer.id + '.json').respond 200 + scope.deleteCustomer(customer) + http.flush() + expect(scope.customers.length).toBe 1 + expect(scope.customers[0]).not.toAngularEqual customer diff --git a/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee b/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee deleted file mode 100644 index 7123055d63..0000000000 --- a/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee +++ /dev/null @@ -1,31 +0,0 @@ -describe "Customers service", -> - Customers = CustomerResource = customers = $httpBackend = null - - beforeEach -> - module 'admin.customers' - - inject ($q, _$httpBackend_, _Customers_, _CustomerResource_) -> - Customers = _Customers_ - CustomerResource = _CustomerResource_ - $httpBackend = _$httpBackend_ - $httpBackend.expectGET('/admin/customers.json?enterprise_id=2').respond 200, [{ id: 5, email: 'someone@email.com'}] - - describe "#index", -> - result = null - - beforeEach -> - expect(Customers.loaded).toBe false - result = Customers.index(enterprise_id: 2) - $httpBackend.flush() - - it "stores returned data in @customers, with ids as keys", -> - # This is super weird and freaking annoying. I think resource results have extra - # properties ($then, $promise) that cause them to not be equal to the reponse object - # provided to the expectGET clause above. - expect(Customers.customers).toEqual [ new CustomerResource({ id: 5, email: 'someone@email.com'}) ] - - it "returns @customers", -> - expect(result).toEqual Customers.customers - - it "sets @loaded to true", -> - expect(Customers.loaded).toBe true 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 index 49ea827900..61d002f23a 100644 --- 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 @@ -8,10 +8,6 @@ describe "InventoryItems service", -> $provide.value 'inventoryItems', inventoryItems null - this.addMatchers - toDeepEqual: (expected) -> - return angular.equals(this.actual, expected) - inject ($q, _$httpBackend_, _InventoryItems_, _InventoryItemResource_) -> InventoryItems = _InventoryItems_ InventoryItemResource = _InventoryItemResource_ diff --git a/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee b/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee new file mode 100644 index 0000000000..1e132ec07d --- /dev/null +++ b/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee @@ -0,0 +1,79 @@ +describe "TagRulesCtrl", -> + ctrl = null + scope = null + enterprise = null + + beforeEach -> + module('admin.tagRules') + enterprise = + id: 45 + tag_groups: [ + { tags: "member", rules: [{ id: 1, preferred_customer_tags: "member" }, { id: 2, preferred_customer_tags: "member" }] }, + { tags: "volunteer", rules: [{ id: 3, preferred_customer_tags: "local" }] } + ] + + inject ($rootScope, $controller) -> + scope = $rootScope + ctrl = $controller 'TagRulesCtrl', {$scope: scope, enterprise: enterprise} + + describe "tagGroup start indices", -> + it "updates on initialization", -> + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 2 + + describe "adding a new tag group", -> + beforeEach -> + scope.addNewRuleTo(scope.tagGroups[0], "DiscountOrder") + + it "adds a new rule of the specified type to the rules array for the tagGroup", -> + expect(scope.tagGroups[0].rules.length).toEqual 3 + expect(scope.tagGroups[0].rules[2].type).toEqual "TagRule::DiscountOrder" + + it "updates tagGroup start indices", -> + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 3 + + describe "deleting a tag group", -> + describe "where the rule is not in the rule list for the tagGroup", -> + beforeEach -> + scope.deleteTagRule(scope.tagGroups[0],scope.tagGroups[1].rules[0]) + + it "does not remove any rules", -> + expect(scope.tagGroups[0].rules.length).toEqual 2 + expect(scope.tagGroups[1].rules.length).toEqual 1 + + describe "with an id", -> + rule = null + + beforeEach inject ($httpBackend) -> + rule = scope.tagGroups[0].rules[0] + spyOn(window, "confirm").andReturn(true) + $httpBackend.expectDELETE('/admin/enterprises/45/tag_rules/1.json').respond(status: 204) + scope.deleteTagRule(scope.tagGroups[0], rule) + $httpBackend.flush() + + it "removes the specified rule from the rules list", -> + expect(scope.tagGroups[0].rules.length).toEqual 1 + expect(scope.tagGroups[1].rules.length).toEqual 1 + expect(scope.tagGroups[0].rules.indexOf(rule)).toEqual -1 + + it "updates tagGroup start indices", -> + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 1 + + describe "without an id", -> + rule = null + + beforeEach inject ($httpBackend) -> + rule = scope.tagGroups[0].rules[0] + rule.id = null + scope.deleteTagRule(scope.tagGroups[0], rule) + + it "removes the specified rule from the rules list", -> + expect(scope.tagGroups[0].rules.length).toEqual 1 + expect(scope.tagGroups[1].rules.length).toEqual 1 + expect(scope.tagGroups[0].rules.indexOf(rule)).toEqual -1 + + it "updates tagGroup start indices", -> + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 1 diff --git a/spec/javascripts/unit/darkswarm/services/map_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/map_spec.js.coffee index 4252000460..669f0ca74d 100644 --- a/spec/javascripts/unit/darkswarm/services/map_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/map_spec.js.coffee @@ -9,6 +9,17 @@ describe "Hubs service", -> orders_close_at: new Date() type: "hub" visible: true + latitude: 0 + longitude: 0 + } + { + id: 3 + active: false + orders_close_at: new Date() + type: "hub" + visible: true + latitude: null + longitude: null } ] @@ -24,3 +35,6 @@ describe "Hubs service", -> it "builds MapMarkers from enterprises", -> expect(OfnMap.enterprises[0].id).toBe enterprises[0].id + + it "excludes enterprises without latitude or longitude", -> + expect(OfnMap.enterprises.map (e) -> e.id).not.toContain enterprises[1].id diff --git a/spec/jobs/heartbeat_job_spec.rb b/spec/jobs/heartbeat_job_spec.rb new file mode 100644 index 0000000000..3bafecd572 --- /dev/null +++ b/spec/jobs/heartbeat_job_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe HeartbeatJob do + context "with time frozen" do + let(:run_time) { Time.zone.local(2016, 4, 13, 13, 0, 0) } + + before { Spree::Config.last_job_queue_heartbeat_at = nil } + + around do |example| + Timecop.freeze(run_time) { example.run } + end + + it "updates the last_job_queue_heartbeat_at config var" do + run_job + Time.parse(Spree::Config.last_job_queue_heartbeat_at).should == run_time + end + end + + + private + + def run_job + clear_jobs + Delayed::Job.enqueue HeartbeatJob.new + flush_jobs ignore_exceptions: false + end +end diff --git a/spec/jobs/products_cache_integrity_checker_job_spec.rb b/spec/jobs/products_cache_integrity_checker_job_spec.rb new file mode 100644 index 0000000000..1770f9bf84 --- /dev/null +++ b/spec/jobs/products_cache_integrity_checker_job_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require 'open_food_network/products_renderer' + +describe ProductsCacheIntegrityCheckerJob do + describe "reporting on differences between the products cache and the current products" do + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle) } + let(:job) { ProductsCacheIntegrityCheckerJob.new distributor.id, order_cycle.id } + + before do + Rails.cache.write "products-json-#{distributor.id}-#{order_cycle.id}", "[1, 2, 3]\n" + OpenFoodNetwork::ProductsRenderer.stub(:new) { double(:pr, products_json: "[1, 3]\n") } + end + + it "reports errors" do + expect(Bugsnag).to receive(:notify) + run_job job + end + + it "deals with nil cached_json" do + Rails.cache.clear + expect(Bugsnag).to receive(:notify) + run_job job + end + end +end diff --git a/spec/jobs/refresh_products_cache_job_spec.rb b/spec/jobs/refresh_products_cache_job_spec.rb new file mode 100644 index 0000000000..d20efe5fad --- /dev/null +++ b/spec/jobs/refresh_products_cache_job_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' +require 'open_food_network/products_renderer' + +describe RefreshProductsCacheJob do + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle) } + + it "renders products and writes them to cache" do + RefreshProductsCacheJob.any_instance.stub(:products_json) { 'products' } + + run_job RefreshProductsCacheJob.new distributor.id, order_cycle.id + + expect(Rails.cache.read("products-json-#{distributor.id}-#{order_cycle.id}")).to eq 'products' + end + + describe "fetching products JSON" do + let(:job) { RefreshProductsCacheJob.new distributor.id, order_cycle.id } + let(:pr) { double(:products_renderer, products_json: nil) } + + it "fetches products JSON" do + expect(OpenFoodNetwork::ProductsRenderer).to receive(:new).with(distributor, order_cycle) { pr } + job.send(:products_json) + end + end +end diff --git a/spec/lib/open_food_network/cached_products_renderer_spec.rb b/spec/lib/open_food_network/cached_products_renderer_spec.rb new file mode 100644 index 0000000000..03b0ab05d5 --- /dev/null +++ b/spec/lib/open_food_network/cached_products_renderer_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' +require 'open_food_network/cached_products_renderer' +require 'open_food_network/products_renderer' + +module OpenFoodNetwork + describe CachedProductsRenderer do + let(:distributor) { double(:distributor, id: 123) } + let(:order_cycle) { double(:order_cycle, id: 456) } + let(:cpr) { CachedProductsRenderer.new(distributor, order_cycle) } + + describe "when the distribution is not set" do + let(:cpr) { CachedProductsRenderer.new(nil, nil) } + + it "raises an exception and returns no products" do + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + end + end + + describe "when the products JSON is already cached" do + before do + Rails.cache.write "products-json-#{distributor.id}-#{order_cycle.id}", 'products' + end + + it "returns the cached JSON" do + expect(cpr.products_json).to eq 'products' + end + + it "raises an exception when there are no products" do + Rails.cache.write "products-json-#{distributor.id}-#{order_cycle.id}", nil + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + end + end + + describe "when the products JSON is not cached" do + let(:cached_json) { Rails.cache.read "products-json-#{distributor.id}-#{order_cycle.id}" } + let(:cache_present) { Rails.cache.exist? "products-json-#{distributor.id}-#{order_cycle.id}" } + + before do + Rails.cache.clear + cpr.stub(:uncached_products_json) { 'fresh products' } + end + + describe "when there are products" do + it "returns products as JSON" do + expect(cpr.products_json).to eq 'fresh products' + end + + it "caches the JSON" do + cpr.products_json + expect(cached_json).to eq 'fresh products' + end + + it "logs a warning" do + cpr.should_receive :log_warning + cpr.products_json + end + end + + describe "when there are no products" do + before { cpr.stub(:uncached_products_json).and_raise ProductsRenderer::NoProducts } + + it "raises an error" do + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + end + + it "caches the products as nil" do + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + expect(cache_present).to be + expect(cached_json).to be_nil + end + + it "logs a warning" do + cpr.should_receive :log_warning + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + end + end + end + + describe "logging a warning" do + it "logs a warning when in production" do + Rails.env.stub(:production?) { true } + expect(Bugsnag).to receive(:notify) + cpr.send(:log_warning) + end + + it "logs a warning when in staging" do + Rails.env.stub(:production?) { false } + Rails.env.stub(:staging?) { true } + expect(Bugsnag).to receive(:notify) + cpr.send(:log_warning) + end + + it "does not log a warning in development or test" do + expect(Bugsnag).to receive(:notify).never + cpr.send(:log_warning) + end + end + + describe "fetching uncached products from ProductsRenderer" do + let(:pr) { double(:products_renderer, products_json: 'uncached products') } + + before do + ProductsRenderer.stub(:new) { pr } + end + + it "returns the uncached products" do + expect(cpr.send(:uncached_products_json)).to eq 'uncached products' + end + end + end +end diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb index e0e7cb40bf..99ebf575af 100644 --- a/spec/lib/open_food_network/order_cycle_management_report_spec.rb +++ b/spec/lib/open_food_network/order_cycle_management_report_spec.rb @@ -86,19 +86,23 @@ module OpenFoodNetwork it "filters to a payment method" do pm2 = create(:payment_method, name: "PM2") - order2 = create(:order) - payment2 = create(:payment, order: order2, payment_method: pm2) + pm3 = create(:payment_method, name: "PM3") + order2 = create(:order, payments: [create(:payment, payment_method: pm2)]) + order3 = create(:order, payments: [create(:payment, payment_method: pm3)]) + # payment2 = create(:payment, order: order2, payment_method: pm2) - subject.stub(:params).and_return(payment_method_name: pm1.name) - subject.filter(orders).should == [order1] + subject.stub(:params).and_return(payment_method_in: [pm1.id, pm3.id] ) + subject.filter(orders).should match_array [order1, order3] end it "filters to a shipping method" do sm2 = create(:shipping_method, name: "ship2") + sm3 = create(:shipping_method, name: "ship3") order2 = create(:order, shipping_method: sm2) + order3 = create(:order, shipping_method: sm3) - subject.stub(:params).and_return(shipping_method_name: sm1.name) - subject.filter(orders).should == [order1] + subject.stub(:params).and_return(shipping_method_in: [sm1.id, sm3.id]) + expect(subject.filter(orders)).to match_array [order1, order3] end it "should do all the filters at once" do diff --git a/spec/lib/open_food_network/products_cache_refreshment_spec.rb b/spec/lib/open_food_network/products_cache_refreshment_spec.rb new file mode 100644 index 0000000000..f65b81346e --- /dev/null +++ b/spec/lib/open_food_network/products_cache_refreshment_spec.rb @@ -0,0 +1,68 @@ +require 'open_food_network/products_cache_refreshment' + +module OpenFoodNetwork + describe ProductsCacheRefreshment do + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle) } + + before { Delayed::Job.destroy_all } + + describe "when there are no tasks enqueued" do + it "enqueues the task" do + expect do + ProductsCacheRefreshment.refresh distributor, order_cycle + end.to enqueue_job RefreshProductsCacheJob + end + + it "enqueues the job with a lower than default priority" do + ProductsCacheRefreshment.refresh distributor, order_cycle + job = Delayed::Job.last + expect(job.priority).to be > Delayed::Worker.default_priority + end + end + + describe "when there is an enqueued task, and it is running" do + before do + job = Delayed::Job.enqueue RefreshProductsCacheJob.new distributor.id, order_cycle.id + job.update_attributes! locked_by: 'asdf', locked_at: Time.now + end + + it "enqueues another task" do + expect do + ProductsCacheRefreshment.refresh distributor, order_cycle + end.to enqueue_job RefreshProductsCacheJob + end + end + + describe "when there are two enqueued tasks, and one is running" do + before do + job1 = Delayed::Job.enqueue RefreshProductsCacheJob.new distributor.id, order_cycle.id + job1.update_attributes! locked_by: 'asdf', locked_at: Time.now + job2 = Delayed::Job.enqueue RefreshProductsCacheJob.new distributor.id, order_cycle.id + end + + it "does not enqueue another task" do + expect do + ProductsCacheRefreshment.refresh distributor, order_cycle + end.not_to enqueue_job RefreshProductsCacheJob + end + end + + describe "enqueuing tasks with different distributions" do + let(:distributor2) { create(:distributor_enterprise) } + let(:order_cycle2) { create(:simple_order_cycle) } + + before do + job1 = Delayed::Job.enqueue RefreshProductsCacheJob.new distributor.id, order_cycle.id + job1.update_attributes! locked_by: 'asdf', locked_at: Time.now + job2 = Delayed::Job.enqueue RefreshProductsCacheJob.new distributor.id, order_cycle.id + end + + it "ignores tasks with differing distributions when choosing whether to enqueue a job" do + expect do + ProductsCacheRefreshment.refresh distributor2, order_cycle2 + end.to enqueue_job RefreshProductsCacheJob + end + end + end +end diff --git a/spec/lib/open_food_network/products_cache_spec.rb b/spec/lib/open_food_network/products_cache_spec.rb new file mode 100644 index 0000000000..a395873f45 --- /dev/null +++ b/spec/lib/open_food_network/products_cache_spec.rb @@ -0,0 +1,418 @@ +require 'open_food_network/products_cache' + +module OpenFoodNetwork + describe ProductsCache do + describe "when a variant changes" do + let(:variant) { create(:variant) } + let(:variant_undistributed) { create(:variant) } + let(:supplier) { create(:supplier_enterprise) } + let(:coordinator) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise) } + let(:oc_undated) { create(:undated_order_cycle, distributors: [distributor], variants: [variant]) } + let(:oc_upcoming) { create(:upcoming_order_cycle, suppliers: [supplier], coordinator: coordinator, distributors: [distributor], variants: [variant]) } + let(:oc_open) { create(:open_order_cycle, distributors: [distributor], variants: [variant]) } + let(:oc_closed) { create(:closed_order_cycle, distributors: [distributor], variants: [variant]) } + + it "refreshes distributions with upcoming order cycles" do + oc_upcoming + expect(ProductsCache).to receive(:refresh_cache).with(distributor, oc_upcoming) + ProductsCache.variant_changed variant + end + + it "refreshes distributions with open order cycles" do + oc_open + expect(ProductsCache).to receive(:refresh_cache).with(distributor, oc_open) + ProductsCache.variant_changed variant + end + + it "does not refresh distributions with undated order cycles" do + oc_undated + expect(ProductsCache).not_to receive(:refresh_cache).with(distributor, oc_undated) + ProductsCache.variant_changed variant + end + + it "does not refresh distributions with closed order cycles" do + oc_closed + expect(ProductsCache).not_to receive(:refresh_cache).with(distributor, oc_closed) + ProductsCache.variant_changed variant + end + + it "limits refresh to outgoing exchanges" do + oc_upcoming + expect(ProductsCache).not_to receive(:refresh_cache).with(coordinator, oc_upcoming) + ProductsCache.variant_changed variant + end + + it "does not refresh distributions where the variant does not appear" do + oc_undated; oc_upcoming; oc_open; oc_closed + variant_undistributed + expect(ProductsCache).not_to receive(:refresh_cache) + ProductsCache.variant_changed variant_undistributed + end + end + + describe "when a variant is destroyed" do + let(:variant) { create(:variant) } + let(:distributor) { create(:distributor_enterprise) } + let!(:oc) { create(:open_order_cycle, distributors: [distributor], variants: [variant]) } + + it "refreshes the cache based on exchanges the variant was in before destruction" do + expect(ProductsCache).to receive(:refresh_cache).with(distributor, oc) + variant.destroy + end + + it "performs the cache refresh after the variant has been destroyed" do + expect(ProductsCache).to receive(:refresh_cache).with(distributor, oc) do + expect(Spree::Variant.where(id: variant.id)).to be_empty + end + + variant.destroy + end + end + + describe "when a product changes" do + let(:product) { create(:simple_product) } + let(:v1) { create(:variant, product: product) } + let(:v2) { create(:variant, product: product) } + let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } + let(:oc) { create(:open_order_cycle) } + let!(:ex1) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d1, variants: [v1]) } + let!(:ex2) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d2, variants: [v1, v2]) } + + before { product.reload } + + it "refreshes the distribution each variant appears in, once each" do + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + expect(ProductsCache).to receive(:refresh_cache).with(d2, oc).once + ProductsCache.product_changed product + end + end + + describe "when a variant override changes" do + let(:variant) { create(:variant) } + let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } + let!(:vo) { create(:variant_override, variant: variant, hub: d1) } + let!(:oc) { create(:open_order_cycle, distributors: [d1, d2], variants: [variant]) } + + it "refreshes the distributions that the variant override affects" do + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + ProductsCache.variant_override_changed vo + end + + it "does not refresh other distributors of the variant" do + expect(ProductsCache).to receive(:refresh_cache).with(d2, oc).never + ProductsCache.variant_override_changed vo + end + end + + + describe "when a variant override is destroyed" do + let(:vo) { double(:variant_override) } + + it "performs the same refresh as a variant override change" do + expect(ProductsCache).to receive(:variant_override_changed).with(vo) + ProductsCache.variant_override_destroyed vo + end + end + + + describe "when a producer property is changed" do + let(:s) { create(:supplier_enterprise) } + let(:pp) { s.producer_properties.last } + let(:product) { create(:simple_product, supplier: s) } + let(:v1) { create(:variant, product: product) } + let(:v2) { create(:variant, product: product) } + let(:v_deleted) { create(:variant, product: product, deleted_at: Time.now) } + let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } + let(:d3) { create(:distributor_enterprise) } + let(:oc) { create(:open_order_cycle) } + let!(:ex1) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d1, variants: [v1]) } + let!(:ex2) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d2, variants: [v1, v2]) } + let!(:ex3) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d3, variants: [product.master, v_deleted]) } + + before do + s.set_producer_property :organic, 'NASAA 12345' + end + + it "refreshes the distributions the supplied variants appear in" do + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + expect(ProductsCache).to receive(:refresh_cache).with(d2, oc).once + ProductsCache.producer_property_changed pp + end + + it "doesn't respond to master or deleted variants" do + expect(ProductsCache).to receive(:refresh_cache).with(d3, oc).never + ProductsCache.producer_property_changed pp + end + end + + + describe "when a producer property is destroyed" do + let(:producer_property) { double(:producer_property) } + + it "triggers the same update as a change to the producer property" do + expect(ProductsCache).to receive(:producer_property_changed).with(producer_property) + ProductsCache.producer_property_destroyed producer_property + end + end + + + describe "when an order cycle is changed" do + let(:variant) { create(:variant) } + let(:s) { create(:supplier_enterprise) } + let(:c) { create(:distributor_enterprise) } + let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } + let!(:oc_open) { create(:open_order_cycle, suppliers: [s], coordinator: c, distributors: [d1, d2], variants: [variant]) } + let!(:oc_upcoming) { create(:upcoming_order_cycle, suppliers: [s], coordinator: c, distributors: [d1, d2], variants: [variant]) } + + before do + oc_open.reload + oc_upcoming.reload + end + + it "updates each outgoing distribution in an upcoming order cycle" do + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc_upcoming).once + expect(ProductsCache).to receive(:refresh_cache).with(d2, oc_upcoming).once + ProductsCache.order_cycle_changed oc_upcoming + end + + it "updates each outgoing distribution in an open order cycle" do + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc_open).once + expect(ProductsCache).to receive(:refresh_cache).with(d2, oc_open).once + ProductsCache.order_cycle_changed oc_open + end + + it "does nothing when the order cycle has been made undated" do + expect(ProductsCache).to receive(:refresh_cache).never + oc_open.orders_open_at = oc_open.orders_close_at = nil + oc_open.save! + end + + it "does nothing when the order cycle has been closed" do + expect(ProductsCache).to receive(:refresh_cache).never + oc_open.orders_open_at = 2.weeks.ago + oc_open.orders_close_at = 1.week.ago + oc_open.save! + end + + it "does not update incoming exchanges" do + expect(ProductsCache).to receive(:refresh_cache).with(c, oc_open).never + ProductsCache.order_cycle_changed oc_open + end + end + + + describe "when an exchange is changed" do + let(:s) { create(:supplier_enterprise) } + let(:c) { create(:distributor_enterprise) } + let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } + let(:v) { create(:variant) } + let(:oc) { create(:open_order_cycle, coordinator: c) } + + describe "incoming exchanges" do + let!(:ex1) { create(:exchange, order_cycle: oc, sender: s, receiver: c, incoming: true, variants: [v]) } + let!(:ex2) { create(:exchange, order_cycle: oc, sender: c, receiver: d1, incoming: false, variants: [v]) } + let!(:ex3) { create(:exchange, order_cycle: oc, sender: c, receiver: d2, incoming: false, variants: []) } + + before { oc.reload } + + it "updates distributions that include one of the supplier's variants" do + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + ProductsCache.exchange_changed ex1 + end + + it "doesn't update distributions that don't include any of the supplier's variants" do + expect(ProductsCache).to receive(:refresh_cache).with(d2, oc).never + ProductsCache.exchange_changed ex1 + end + end + + describe "outgoing exchanges" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: c, receiver: d1, incoming: false) } + + it "does not update for undated order cycles" do + oc.update_attributes! orders_open_at: nil, orders_close_at: nil + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).never + ProductsCache.exchange_changed ex + end + + it "updates for upcoming order cycles" do + oc.update_attributes! orders_open_at: 1.week.from_now, orders_close_at: 2.weeks.from_now + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + ProductsCache.exchange_changed ex + end + + it "updates for open order cycles" do + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + ProductsCache.exchange_changed ex + end + + it "does not update for closed order cycles" do + oc.update_attributes! orders_open_at: 2.weeks.ago, orders_close_at: 1.week.ago + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).never + ProductsCache.exchange_changed ex + end + end + end + + + describe "when an exchange is destroyed" do + let(:exchange) { double(:exchange) } + + it "triggers the same update as a change to the exchange" do + expect(ProductsCache).to receive(:exchange_changed).with(exchange) + ProductsCache.exchange_destroyed exchange + end + end + + + describe "when an enterprise fee is changed" do + let(:s) { create(:supplier_enterprise) } + let(:c) { create(:distributor_enterprise) } + let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } + let(:ef) { create(:enterprise_fee) } + let(:ef_coord) { create(:enterprise_fee, order_cycles: [oc]) } + let(:oc) { create(:open_order_cycle, coordinator: c) } + + + describe "updating exchanges when it's a supplier fee" do + let(:v) { create(:variant) } + let!(:ex1) { create(:exchange, order_cycle: oc, sender: s, receiver: c, incoming: true, variants: [v], enterprise_fees: [ef]) } + let!(:ex2) { create(:exchange, order_cycle: oc, sender: c, receiver: d1, incoming: false, variants: [v]) } + let!(:ex3) { create(:exchange, order_cycle: oc, sender: c, receiver: d2, incoming: false, variants: []) } + + before { ef.reload } + + describe "updating distributions that include one of the supplier's variants" do + it "does not update undated order cycles" do + oc.update_attributes! orders_open_at: nil, orders_close_at: nil + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).never + ProductsCache.enterprise_fee_changed ef + end + + it "updates upcoming order cycles" do + oc.update_attributes! orders_open_at: 1.week.from_now, orders_close_at: 2.weeks.from_now + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + ProductsCache.enterprise_fee_changed ef + end + + it "updates open order cycles" do + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + ProductsCache.enterprise_fee_changed ef + end + + it "does not update closed order cycles" do + oc.update_attributes! orders_open_at: 2.weeks.ago, orders_close_at: 1.week.ago + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).never + ProductsCache.enterprise_fee_changed ef + end + end + + it "doesn't update distributions that don't include any of the supplier's variants" do + expect(ProductsCache).to receive(:refresh_cache).with(d2, oc).never + ProductsCache.enterprise_fee_changed ef + end + end + + + it "updates order cycles when it's a coordinator fee" do + ef_coord + expect(ProductsCache).to receive(:order_cycle_changed).with(oc).once + ProductsCache.enterprise_fee_changed ef_coord + end + + + describe "updating exchanges when it's a distributor fee" do + let(:ex0) { create(:exchange, order_cycle: oc, sender: s, receiver: c, incoming: true, enterprise_fees: [ef]) } + let(:ex1) { create(:exchange, order_cycle: oc, sender: c, receiver: d1, incoming: false, enterprise_fees: [ef]) } + let(:ex2) { create(:exchange, order_cycle: oc, sender: c, receiver: d2, incoming: false, enterprise_fees: []) } + + describe "updating distributions that include the fee" do + it "does not update undated order cycles" do + oc.update_attributes! orders_open_at: nil, orders_close_at: nil + ex1 + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).never + ProductsCache.enterprise_fee_changed ef + end + + it "updates upcoming order cycles" do + oc.update_attributes! orders_open_at: 1.week.from_now, orders_close_at: 2.weeks.from_now + ex1 + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + ProductsCache.enterprise_fee_changed ef + end + + it "updates open order cycles" do + ex1 + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).once + ProductsCache.enterprise_fee_changed ef + end + + it "does not update closed order cycles" do + oc.update_attributes! orders_open_at: 2.weeks.ago, orders_close_at: 1.week.ago + ex1 + expect(ProductsCache).to receive(:refresh_cache).with(d1, oc).never + ProductsCache.enterprise_fee_changed ef + end + end + + it "doesn't update exchanges that don't include the fee" do + ex1; ex2 + expect(ProductsCache).to receive(:refresh_cache).with(d2, oc).never + ProductsCache.enterprise_fee_changed ef + end + + it "doesn't update incoming exchanges" do + ex0 + expect(ProductsCache).to receive(:refresh_cache).with(c, oc).never + ProductsCache.enterprise_fee_changed ef + end + end + end + + describe "when a distributor enterprise is changed" do + let(:d) { create(:distributor_enterprise) } + let(:oc) { create(:open_order_cycle, distributors: [d]) } + + it "updates each distribution the enterprise is active in" do + expect(ProductsCache).to receive(:refresh_cache).with(d, oc) + ProductsCache.distributor_changed d + end + end + + describe "when an inventory item is changed" do + let!(:d) { create(:distributor_enterprise) } + let!(:v) { create(:variant) } + let!(:oc1) { create(:open_order_cycle, distributors: [d], variants: [v]) } + let(:oc2) { create(:open_order_cycle, distributors: [d], variants: []) } + let!(:ii) { create(:inventory_item, enterprise: d, variant: v) } + + it "updates each distribution for that enterprise+variant" do + expect(ProductsCache).to receive(:refresh_cache).with(d, oc1) + ProductsCache.inventory_item_changed ii + end + + it "doesn't update distributions that don't feature the variant" do + oc2 + expect(ProductsCache).to receive(:refresh_cache).with(d, oc2).never + ProductsCache.inventory_item_changed ii + end + end + + describe "refreshing the cache" do + let(:distributor) { double(:distributor) } + let(:order_cycle) { double(:order_cycle) } + + it "notifies ProductsCacheRefreshment" do + expect(ProductsCacheRefreshment).to receive(:refresh).with(distributor, order_cycle) + ProductsCache.send(:refresh_cache, distributor, order_cycle) + end + 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 index a231fd3688..1069c9946c 100644 --- a/spec/lib/open_food_network/products_renderer_spec.rb +++ b/spec/lib/open_food_network/products_renderer_spec.rb @@ -3,10 +3,10 @@ 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) } + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } + let(:exchange) { order_cycle.exchanges.to_enterprises(distributor).outgoing.first } + let(:pr) { ProductsRenderer.new(distributor, order_cycle) } describe "sorting" do let(:t1) { create(:taxon) } @@ -24,14 +24,14 @@ module OpenFoodNetwork 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) + distributor.stub(:preferred_shopfront_taxon_order) {"#{t1.id},#{t2.id}"} + products = pr.send(:load_products) 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) + distributor.stub(:preferred_shopfront_taxon_order) {""} + products = pr.send(:load_products) products.should == [p1, p2, p3, p4] end end @@ -45,17 +45,17 @@ module OpenFoodNetwork end it "only returns products for the current order cycle" do - pr.products.should include product.name + pr.products_json.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 + pr.products_json.should_not include product.name end it "strips html from description" do product.update_attribute(:description, "turtles frogs") - json = pr.products + json = pr.products_json json.should include "frogs" json.should_not include " ["48x48>", :png]} end end + + describe "callbacks" do + let!(:product) { create(:simple_product) } + + let!(:image_file) { File.open("#{Rails.root}/app/assets/images/logo-white.png") } + let!(:image) { Image.create(viewable_id: product.master.id, viewable_type: 'Spree::Variant', alt: "image", attachment: image_file) } + + it "refreshes the products cache when changed" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + image.alt = 'asdf' + image.save + end + + it "refreshes the products cache when destroyed" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + image.destroy + end + end end end diff --git a/spec/models/spree/option_type_spec.rb b/spec/models/spree/option_type_spec.rb new file mode 100644 index 0000000000..cfc0654733 --- /dev/null +++ b/spec/models/spree/option_type_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +module Spree + describe OptionType do + describe "products cache" do + let!(:product) { create(:simple_product, option_types: [option_type]) } + let(:variant) { product.variants.first } + let(:option_type) { create(:option_type) } + let(:option_value) { create(:option_value, option_type: option_type) } + + before do + option_type.reload + variant.option_values << option_value + end + + it "refreshes the products cache on change, via product" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + option_type.name = 'foo' + option_type.save! + end + + it "refreshes the products cache on destruction, via option value destruction" do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).with(variant) + option_type.destroy + end + end + end +end diff --git a/spec/models/spree/option_value_spec.rb b/spec/models/spree/option_value_spec.rb new file mode 100644 index 0000000000..f784e08af2 --- /dev/null +++ b/spec/models/spree/option_value_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +module Spree + describe OptionValue do + describe "products cache" do + let(:variant) { create(:variant) } + let(:option_value) { create(:option_value) } + + before do + variant.option_values << option_value + option_value.reload + end + + it "refreshes the products cache on change, via variant" do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).with(variant) + option_value.name = 'foo' + option_value.save! + end + + it "refreshes the products cache on destruction, via variant" do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).with(variant) + option_value.destroy + end + end + end +end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 4d681e01cc..ab86be1580 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -108,44 +108,76 @@ describe Spree::Order do subject.update_distribution_charge! end - describe "looking up whether a line item can be provided by an order cycle" do - it "returns true when the variant is provided" do - v = double(:variant) - line_item = double(:line_item, variant: v) - order_cycle = double(:order_cycle, variants: [v]) - subject.stub(:order_cycle) { order_cycle } + context "appying tag rules" do + let(:enterprise) { create(:distributor_enterprise) } + let(:customer) { create(:customer, enterprise: enterprise, tag_list: "tagtagtag") } + let(:tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "tagtagtag") } + let(:order) { create(:order_with_totals_and_distribution, distributor: enterprise, customer: customer) } - subject.send(:provided_by_order_cycle?, line_item).should be_true + before do + tag_rule.calculator.update_attribute(:preferred_flat_percent, -10) end - it "returns false otherwise" do - v = double(:variant) - line_item = double(:line_item, variant: v) - order_cycle = double(:order_cycle, variants: []) - subject.stub(:order_cycle) { order_cycle } - - subject.send(:provided_by_order_cycle?, line_item).should be_false + context "when the rule applies" do + it "applies the rule" do + order.update_distribution_charge! + order.reload + discount = order.adjustments.find_by_label("Discount") + expect(discount).to be_a Spree::Adjustment + expect(discount.amount).to eq (order.item_total / -10).round(2) + end end - it "returns false when there is no order cycle" do - v = double(:variant) - line_item = double(:line_item, variant: v) - subject.stub(:order_cycle) { nil } + context "when the rule does not apply" do + before { tag_rule.update_attribute(:preferred_customer_tags, "tagtag") } - subject.send(:provided_by_order_cycle?, line_item).should be_false + it "does not apply the rule" do + order.update_distribution_charge! + order.reload + discount = order.adjustments.find_by_label("Discount") + expect(discount).to be_nil + end end end + end - it "looks up product distribution enterprise fees for a line item" do - product = double(:product) - variant = double(:variant, product: product) - line_item = double(:line_item, variant: variant) + describe "looking up whether a line item can be provided by an order cycle" do + it "returns true when the variant is provided" do + v = double(:variant) + line_item = double(:line_item, variant: v) + order_cycle = double(:order_cycle, variants: [v]) + subject.stub(:order_cycle) { order_cycle } - product_distribution = double(:product_distribution) - product.should_receive(:product_distribution_for).with(subject.distributor) { product_distribution } - - subject.send(:product_distribution_for, line_item).should == product_distribution + subject.send(:provided_by_order_cycle?, line_item).should be_true end + + it "returns false otherwise" do + v = double(:variant) + line_item = double(:line_item, variant: v) + order_cycle = double(:order_cycle, variants: []) + subject.stub(:order_cycle) { order_cycle } + + subject.send(:provided_by_order_cycle?, line_item).should be_false + end + + it "returns false when there is no order cycle" do + v = double(:variant) + line_item = double(:line_item, variant: v) + subject.stub(:order_cycle) { nil } + + subject.send(:provided_by_order_cycle?, line_item).should be_false + end + end + + it "looks up product distribution enterprise fees for a line item" do + product = double(:product) + variant = double(:variant, product: product) + line_item = double(:line_item, variant: variant) + + product_distribution = double(:product_distribution) + product.should_receive(:product_distribution_for).with(subject.distributor) { product_distribution } + + subject.send(:product_distribution_for, line_item).should == product_distribution end describe "getting the admin and handling charge" do @@ -457,33 +489,6 @@ describe Spree::Order do Spree::Order.not_state(:canceled).should_not include o end end - - describe "with payment method names" do - let!(:o1) { create(:order) } - let!(:o2) { create(:order) } - let!(:pm1) { create(:payment_method, name: 'foo') } - let!(:pm2) { create(:payment_method, name: 'bar') } - let!(:p1) { create(:payment, order: o1, payment_method: pm1) } - let!(:p2) { create(:payment, order: o2, payment_method: pm2) } - - it "returns the order with payment method name when one specified" do - Spree::Order.with_payment_method_name('foo').should == [o1] - end - - it "returns the orders with payment method name when many specified" do - Spree::Order.with_payment_method_name(['foo', 'bar']).should include o1, o2 - end - - it "doesn't return rows with a different payment method name" do - Spree::Order.with_payment_method_name('foobar').should_not include o1 - Spree::Order.with_payment_method_name('foobar').should_not include o2 - end - - it "doesn't return duplicate rows" do - p2 = FactoryGirl.create(:payment, order: o1, payment_method: pm1) - Spree::Order.with_payment_method_name('foo').length.should == 1 - end - end end describe "shipping address prepopulation" do @@ -559,39 +564,76 @@ describe Spree::Order do end describe "associating a customer" do - let(:user) { create(:user) } let(:distributor) { create(:distributor_enterprise) } + let!(:order) { create(:order, distributor: distributor) } - context "when a user has been set on the order" do - let!(:order) { create(:order, distributor: distributor, user: user) } - context "and a customer for order.distributor and order.user.email already exists" do - let!(:customer) { create(:customer, enterprise: distributor, email: user.email) } - it "associates the order with the existing customer" do - order.send(:associate_customer) + context "when an email address is available for the order" do + before { allow(order).to receive(:email_for_customer) { "existing@email.com" }} + + context "and a customer for order.distributor and order#email_for_customer already exists" do + let!(:customer) { create(:customer, enterprise: distributor, email: "existing@email.com" ) } + + it "associates the order with the existing customer, and returns the customer" do + result = order.send(:associate_customer) expect(order.customer).to eq customer + expect(result).to eq customer end end + context "and a customer for order.distributor and order.user.email does not alread exist" do let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } - it "creates a new customer" do - expect{order.send(:associate_customer)}.to change{Customer.count}.by 1 + + it "does not set the customer and returns nil" do + result = order.send(:associate_customer) + expect(order.customer).to be_nil + expect(result).to be_nil end end end - context "when a user has not been set on the order" do - let!(:order) { create(:order, distributor: distributor, user: nil) } - context "and a customer for order.distributor and order.email already exists" do - let!(:customer) { create(:customer, enterprise: distributor, email: order.email) } - it "creates a new customer" do - order.send(:associate_customer) + context "when an email address is not available for the order" do + let!(:customer) { create(:customer, enterprise: distributor) } + before { allow(order).to receive(:email_for_customer) { nil }} + + it "does not set the customer and returns nil" do + result = order.send(:associate_customer) + expect(order.customer).to be_nil + expect(result).to be_nil + end + end + end + + describe "ensuring a customer is linked" do + let(:distributor) { create(:distributor_enterprise) } + let!(:order) { create(:order, distributor: distributor) } + + context "when a customer has already been linked to the order" do + let!(:customer) { create(:customer, enterprise: distributor, email: "existing@email.com" ) } + before { order.update_attribute(:customer_id, customer.id) } + + it "does nothing" do + order.send(:ensure_customer) + expect(order.customer).to eq customer + end + end + + context "when a customer not been linked to the order" do + context "but one matching order#email_for_customer already exists" do + let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } + before { allow(order).to receive(:email_for_customer) { 'some-other-email@email.com' } } + + it "links the customer customer to the order" do + expect(order.customer).to be_nil + expect{order.send(:ensure_customer)}.to_not change{Customer.count} expect(order.customer).to eq customer end end - context "and a customer for order.distributor and order.email does not alread exist" do - let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } + + context "and order#email_for_customer does not match any existing customers" do it "creates a new customer" do - expect{order.send(:associate_customer)}.to change{Customer.count}.by 1 + expect(order.customer).to be_nil + expect{order.send(:ensure_customer)}.to change{Customer.count}.by 1 + expect(order.customer).to be_a Customer end end end diff --git a/spec/models/spree/preference_spec.rb b/spec/models/spree/preference_spec.rb new file mode 100644 index 0000000000..ff71c9c9c6 --- /dev/null +++ b/spec/models/spree/preference_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +module Spree + describe Preference do + describe "refreshing the products cache" do + it "reports when product_selection_from_inventory_only has changed" do + p = Preference.new(key: 'enterprise/product_selection_from_inventory_only/123') + expect(p.send(:product_selection_from_inventory_only_changed?)).to be_true + end + + it "reports when product_selection_from_inventory_only has not changed" do + p = Preference.new(key: 'enterprise/shopfront_message/123') + expect(p.send(:product_selection_from_inventory_only_changed?)).to be_false + end + + it "looks up the referenced enterprise" do + e = create(:distributor_enterprise) + p = Preference.new(key: "enterprise/product_selection_from_inventory_only/#{e.id}") + expect(p.send(:enterprise)).to eql e + end + end + end +end diff --git a/spec/models/spree/price_spec.rb b/spec/models/spree/price_spec.rb new file mode 100644 index 0000000000..5d48afd95b --- /dev/null +++ b/spec/models/spree/price_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +module Spree + describe Price do + describe "callbacks" do + let(:variant) { create(:variant) } + let(:price) { variant.default_price } + + it "refreshes the products cache on change" do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).with(variant) + price.amount = 123 + price.save + end + + # Do not refresh on price destruction - this (only?) happens when variant is destroyed, + # and in that case the variant will take responsibility for refreshing the cache + + it "does not refresh the cache when variant is not set" do + # Creates a price without the back link to variant + create(:product, master: create(:variant)) + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).never + end + end + end +end diff --git a/spec/models/spree/product_property_spec.rb b/spec/models/spree/product_property_spec.rb new file mode 100644 index 0000000000..1c9799ab29 --- /dev/null +++ b/spec/models/spree/product_property_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +module Spree + describe ProductProperty do + describe "callbacks" do + let(:product) { product_property.product } + let(:product_property) { create(:product_property) } + + it "refreshes the products cache on save, via Product" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + product_property.value = 123 + product_property.save + end + + it "refreshes the products cache on destroy" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + product_property.destroy + end + end + end +end diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index c37b49bc23..9c2ee6b664 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -161,6 +161,19 @@ module Spree end end + describe "callbacks" do + let(:product) { create(:simple_product) } + + it "refreshes the products cache on save" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + product.name = 'asdf' + product.save + end + + # On destroy, all distributed variants are refreshed by a Variant around_destroy + # callback, so we don't need to do anything on the product model. + end + describe "scopes" do describe "in_supplier" do it "shows products in supplier" do diff --git a/spec/models/spree/property_spec.rb b/spec/models/spree/property_spec.rb new file mode 100644 index 0000000000..a86513b5a9 --- /dev/null +++ b/spec/models/spree/property_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +module Spree + describe Property do + describe "callbacks" do + let(:property) { product_property.property } + let(:product) { product_property.product } + let(:product_property) { create(:product_property) } + + it "refreshes the products cache on save" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + property.name = 'asdf' + property.save + end + end + end +end diff --git a/spec/models/spree/shipping_method_spec.rb b/spec/models/spree/shipping_method_spec.rb index 0e738470dd..33c44b8b6d 100644 --- a/spec/models/spree/shipping_method_spec.rb +++ b/spec/models/spree/shipping_method_spec.rb @@ -37,21 +37,28 @@ module Spree describe "availability" do - let(:sm) { build(:shipping_method) } + let(:sm) { create(:shipping_method) } + let(:currency) { 'AUD' } + + before do + sm.calculator.preferred_currency = currency + end it "is available to orders that match its distributor" do - o = build(:order, ship_address: build(:address), distributor: sm.distributors.first) + o = create(:order, ship_address: create(:address), + distributor: sm.distributors.first, currency: currency) sm.should be_available_to_order o end it "is not available to orders that do not match its distributor" do - o = build(:order, ship_address: build(:address), - distributor: build(:distributor_enterprise)) + o = create(:order, ship_address: create(:address), + distributor: create(:distributor_enterprise), currency: currency) sm.should_not be_available_to_order o end it "is available to orders with no shipping address" do - o = build(:order, ship_address: nil, distributor: sm.distributors.first) + o = create(:order, ship_address: nil, + distributor: sm.distributors.first, currency: currency) sm.should be_available_to_order o end end diff --git a/spec/models/spree/taxon_spec.rb b/spec/models/spree/taxon_spec.rb index a0d729c054..926e5b3c5f 100644 --- a/spec/models/spree/taxon_spec.rb +++ b/spec/models/spree/taxon_spec.rb @@ -7,6 +7,21 @@ module Spree let(:t1) { create(:taxon) } let(:t2) { create(:taxon) } + describe "callbacks" do + let(:product) { create(:simple_product, taxons: [t1]) } + + it "refreshes the products cache on save" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + t1.name = 'asdf' + t1.save + end + + it "refreshes the products cache on destroy" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + t1.destroy + end + end + describe "finding all supplied taxons" do let!(:p1) { create(:simple_product, supplier: e, taxons: [t1, t2]) } diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 7fefd56afb..923bc93183 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -17,11 +17,11 @@ describe Spree.user_class do it "enforces the limit on the number of enterprise owned" do expect(u2.owned_enterprises(:reload)).to eq [] u2.owned_enterprises << e1 - expect(u2.save!).to_not raise_error - expect { + expect { u2.save! }.to_not raise_error + expect do u2.owned_enterprises << e2 u2.save! - }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: #{u2.email} is not permitted to own any more enterprises (limit is 1)." + end.to raise_error ActiveRecord::RecordInvalid, "Validation failed: #{u2.email} is not permitted to own any more enterprises (limit is 1)." end end @@ -53,6 +53,23 @@ describe Spree.user_class do create(:user) end.to enqueue_job ConfirmSignupJob end + + it "should not create a customer" do + expect do + create(:user) + end.to change(Customer, :count).by(0) + end + + describe "when a customer record exists" do + let!(:customer) { create(:customer, user: nil) } + + it "should not create a customer" do + expect(customer.user).to be nil + user = create(:user, email: customer.email) + customer.reload + expect(customer.user).to eq user + end + end end describe "known_users" do @@ -65,9 +82,9 @@ describe Spree.user_class do it "returns a list of users which manage shared enterprises" do expect(u1.known_users).to include u1, u2 expect(u1.known_users).to_not include u3 - expect(u2.known_users).to include u1,u2 + expect(u2.known_users).to include u1, u2 expect(u2.known_users).to_not include u3 - expect(u3.known_users).to_not include u1,u2,u3 + expect(u3.known_users).to_not include u1, u2, u3 end end @@ -79,4 +96,43 @@ describe Spree.user_class do end end end + + describe "retrieving orders for /account page" do + let!(:u1) { create(:user) } + let!(:u2) { create(:user) } + let!(:distributor1) { create(:distributor_enterprise) } + let!(:distributor2) { create(:distributor_enterprise) } + let!(:d1o1) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id) } + let!(:d1o2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id) } + let!(:d1_order_for_u2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u2.id) } + let!(:d1o3) { create(:order, state: 'cart', distributor: distributor1, user_id: u1.id) } + let!(:d2o1) { create(:completed_order_with_totals, distributor: distributor2, user_id: u2.id) } + + let!(:completed_payment) { create(:payment, order: d1o1, state: 'completed') } + let!(:payment) { create(:payment, order: d1o2, state: 'invalid') } + + it "returns enterprises that the user has ordered from" do + expect(u1.enterprises_ordered_from).to eq [distributor1.id] + end + + it "returns orders and payments for the user, organised by distributor" do + expect(u1.orders_by_distributor).to include distributor1 + expect(u1.orders_by_distributor.first.distributed_orders).to include d1o1 + end + + it "doesn't return irrelevant distributors" do + expect(u1.orders_by_distributor).not_to include distributor2 + end + it "doesn't return other users' orders" do + expect(u1.orders_by_distributor.first.distributed_orders).not_to include d1_order_for_u2 + end + + it "doesn't return uncompleted orders" do + expect(u1.orders_by_distributor.first.distributed_orders).not_to include d1o3 + end + + it "doesn't return uncompleted payments" do + expect(u1.orders_by_distributor.first.distributed_orders.map(&:payments).flatten).not_to include payment + end + end end diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 82eed6ba63..a6c24bd3d3 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' require 'open_food_network/option_value_namer' +require 'open_food_network/products_cache' module Spree describe Variant do @@ -163,6 +164,40 @@ module Spree end end + describe "callbacks" do + let(:variant) { create(:variant) } + + it "refreshes the products cache on save" do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).with(variant) + variant.sku = 'abc123' + variant.save + end + + it "refreshes the products cache on destroy" do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant) + variant.destroy + end + + context "when it is the master variant" do + let(:product) { create(:simple_product) } + let(:master) { product.master } + + it "refreshes the products cache for the entire product on save" do + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).never + master.sku = 'abc123' + master.save + end + + it "refreshes the products cache for the entire product on destroy" do + # Does this ever happen? + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).never + master.destroy + end + end + end + describe "indexing variants by id" do let!(:v1) { create(:variant) } let!(:v2) { create(:variant) } diff --git a/spec/models/tag_rule/discount_order_spec.rb b/spec/models/tag_rule/discount_order_spec.rb new file mode 100644 index 0000000000..dab901dfbf --- /dev/null +++ b/spec/models/tag_rule/discount_order_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe TagRule::DiscountOrder, type: :model do + let!(:tag_rule) { create(:tag_rule) } + + describe "determining relevance based on additional requirements" do + let(:subject) { double(:subject) } + + before do + tag_rule.set_context(subject,{}) + allow(tag_rule).to receive(:customer_tags_match?) { true } + allow(subject).to receive(:class) { Spree::Order } + end + + context "when already_applied? returns false" do + before { expect(tag_rule).to receive(:already_applied?) { false } } + + it "returns true" do + expect(tag_rule.send(:relevant?)).to be true + end + end + + context "when already_applied? returns true" do + before { expect(tag_rule).to receive(:already_applied?) { true } } + + it "returns false immediately" do + expect(tag_rule.send(:relevant?)).to be false + end + end + end + + describe "determining whether a the rule has already been applied to an order" do + let!(:order) { create(:order) } + let!(:adjustment) { order.adjustments.create({:amount => 12.34, :source => order, :originator => tag_rule, :label => 'discount' }, :without_protection => true) } + + before do + tag_rule.set_context(order, nil) + end + + context "where adjustments originating from the rule already exist" do + it { expect(tag_rule.send(:already_applied?)).to be true} + end + + context "where existing adjustments originate from other rules" do + before { adjustment.update_attribute(:originator_id,create(:tag_rule).id) } + it { expect(tag_rule.send(:already_applied?)).to be false} + end + end + + describe "applying the rule" do + # Assume that all validation is done by the TagRule base class + + let!(:line_item) { create(:line_item, price: 100.00) } + let!(:order) { line_item.order } + + before do + order.update_distribution_charge! + tag_rule.calculator.update_attribute(:preferred_flat_percent, -10.00) + tag_rule.set_context(order, nil) + end + + context "in a simple scenario" do + let(:adjustment) { order.reload.adjustments.where(originator_id: tag_rule, originator_type: "TagRule").first } + + it "creates a new adjustment on the order" do + tag_rule.send(:apply!) + expect(adjustment).to be_a Spree::Adjustment + expect(adjustment.amount).to eq -10.00 + expect(adjustment.label).to eq "Discount" + expect(order.adjustment_total).to eq -10.00 + expect(order.total).to eq 90.00 + end + end + + context "when shipping charges apply" do + let!(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new( preferred_amount: 25.00 ) ) } + before do + shipping_method.create_adjustment("Shipping", order, order, true) + end + + let(:adjustment) { order.reload.adjustments.where(originator_id: tag_rule, originator_type: "TagRule").first } + + it "the adjustment is made on line item total, ie. ignores the shipping amount" do + tag_rule.send(:apply!) + expect(adjustment).to be_a Spree::Adjustment + expect(adjustment.amount).to eq -10.00 + expect(adjustment.label).to eq "Discount" + expect(order.adjustment_total).to eq 15.00 + expect(order.total).to eq 115.00 + end + end + end +end diff --git a/spec/models/tag_rule/filter_shipping_methods_spec.rb b/spec/models/tag_rule/filter_shipping_methods_spec.rb new file mode 100644 index 0000000000..539aa3c6ca --- /dev/null +++ b/spec/models/tag_rule/filter_shipping_methods_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe TagRule::DiscountOrder, type: :model do + let!(:tag_rule) { create(:filter_shipping_methods_tag_rule) } + + describe "determining whether tags match for a given shipping method" do + context "when the shipping method is nil" do + + it "returns false" do + expect(tag_rule.send(:tags_match?, nil)).to be false + end + end + + context "when the shipping method is not nil" do + let(:shipping_method) { create(:shipping_method, tag_list: ["member","local","volunteer"]) } + + context "when the rule has no preferred shipping method tags specified" do + before { allow(tag_rule).to receive(:preferred_shipping_method_tags) { "" } } + it { expect(tag_rule.send(:tags_match?, shipping_method)).to be false } + end + + context "when the rule has preferred customer tags specified that match ANY of the customer tags" do + before { allow(tag_rule).to receive(:preferred_shipping_method_tags) { "wholesale,some_tag,member" } } + it { expect(tag_rule.send(:tags_match?, shipping_method)).to be true } + end + + context "when the rule has preferred customer tags specified that match NONE of the customer tags" do + before { allow(tag_rule).to receive(:preferred_shipping_method_tags) { "wholesale,some_tag,some_other_tag" } } + it { expect(tag_rule.send(:tags_match?, shipping_method)).to be false } + end + end + end + + describe "applying the rule" do + # Assume that all validation is done by the TagRule base class + + let(:sm1) { create(:shipping_method, tag_list: ["tag1", "something", "somethingelse"]) } + let(:sm2) { create(:shipping_method, tag_list: ["tag2"]) } + let(:sm3) { create(:shipping_method, tag_list: ["tag3"]) } + let!(:shipping_methods) { [sm1, sm2, sm3] } + + before do + tag_rule.update_attribute(:preferred_shipping_method_tags, "tag2") + tag_rule.set_context(shipping_methods, nil) + end + + context "apply!" do + context "when showing matching shipping methods" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "visible") } + it "does nothing" do + tag_rule.send(:apply!) + expect(shipping_methods).to eq [sm1, sm2, sm3] + end + end + + context "when hiding matching shipping methods" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "hidden") } + it "removes matching shipping methods from the list" do + tag_rule.send(:apply!) + expect(shipping_methods).to eq [sm1, sm3] + end + end + end + + context "apply_default!" do + context "when showing matching shipping methods" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "visible") } + it "remove matching shipping methods from the list" do + tag_rule.send(:apply_default!) + expect(shipping_methods).to eq [sm1, sm3] + end + end + + context "when hiding matching shipping methods" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "hidden") } + it "does nothing" do + tag_rule.send(:apply_default!) + expect(shipping_methods).to eq [sm1, sm2, sm3] + end + end + end + end +end diff --git a/spec/models/tag_rule_spec.rb b/spec/models/tag_rule_spec.rb new file mode 100644 index 0000000000..549c2f88aa --- /dev/null +++ b/spec/models/tag_rule_spec.rb @@ -0,0 +1,201 @@ +require 'spec_helper' + +describe TagRule, type: :model do + let!(:tag_rule) { create(:tag_rule) } + + describe "validations" do + it "requires a enterprise" do + expect(tag_rule).to validate_presence_of :enterprise + end + end + + describe 'setting the context' do + let(:subject) { double(:subject) } + let(:context) { double(:context) } + it "stores the subject and context provided as instance variables on the model" do + tag_rule.set_context(subject, context) + expect(tag_rule.subject).to eq subject + expect(tag_rule.context).to eq context + expect(tag_rule.instance_variable_get(:@subject)).to eq subject + expect(tag_rule.instance_variable_get(:@context)).to eq context + end + end + + describe "determining relevance based on subject and context" do + context "when the subject is nil" do + it "returns false" do + expect(tag_rule.send(:relevant?)).to be false + end + end + + context "when the subject is not nil" do + let(:subject) { double(:subject) } + + before do + tag_rule.set_context(subject,{}) + allow(tag_rule).to receive(:customer_tags_match?) { :customer_tags_match_result } + allow(tag_rule).to receive(:subject_class) { Spree::Order} + end + + + context "when the subject class matches tag_rule#subject_class" do + before do + allow(subject).to receive(:class) { Spree::Order } + end + + context "when the rule does not repond to #additional_requirements_met?" do + before { allow(tag_rule).to receive(:respond_to?).with(:additional_requirements_met?, true) { false } } + + it "returns true" do + expect(tag_rule.send(:relevant?)).to be true + end + end + + context "when the rule reponds to #additional_requirements_met?" do + before { allow(tag_rule).to receive(:respond_to?).with(:additional_requirements_met?, true) { true } } + + context "and #additional_requirements_met? returns a truthy value" do + before { allow(tag_rule).to receive(:additional_requirements_met?) { "smeg" } } + + it "returns true immediately" do + expect(tag_rule.send(:relevant?)).to be true + end + end + + context "and #additional_requirements_met? returns true" do + before { allow(tag_rule).to receive(:additional_requirements_met?) { true } } + + it "returns true immediately" do + expect(tag_rule.send(:relevant?)).to be true + end + end + + context "and #additional_requirements_met? returns false" do + before { allow(tag_rule).to receive(:additional_requirements_met?) { false } } + + it "returns false immediately" do + expect(tag_rule.send(:relevant?)).to be false + end + end + end + end + + context "when the subject class does not match tag_rule#subject_class" do + before do + allow(subject).to receive(:class) { Spree::LineItem } + end + + it "returns false immediately" do + expect(tag_rule.send(:relevant?)).to be false + expect(tag_rule).to_not have_received :customer_tags_match? + end + end + end + + describe "determining whether specified customer tags match the given context" do + context "when the context is nil" do + before { tag_rule.set_context(nil, nil) } + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be false + end + end + + context "when the context has no customer specified" do + let(:context) { { something_that_is_not_a_customer: double(:something) } } + + before { tag_rule.set_context(nil, context) } + + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be false + end + end + + context "when the context has a customer specified" do + let(:context) { { customer: double(:customer, tag_list: ["member","local","volunteer"] ) } } + + before { tag_rule.set_context(nil, context) } + + context "when the rule has no preferred customer tags specified" do + before do + allow(tag_rule).to receive(:preferred_customer_tags) { "" } + end + + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be false + end + end + + context "when the rule has preferred customer tags specified that match ANY of the customer tags" do + before do + allow(tag_rule).to receive(:preferred_customer_tags) { "wholesale,some_tag,member" } + end + + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be true + end + end + + context "when the rule has preferred customer tags specified that match NONE of the customer tags" do + before do + allow(tag_rule).to receive(:preferred_customer_tags) { "wholesale,some_tag,some_other_tag" } + end + + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be false + end + end + end + end + + describe "applying a tag rule to a subject" do + before { allow(tag_rule).to receive(:apply!) } + + context "when the rule is deemed to be relevant" do + before { allow(tag_rule).to receive(:relevant?) { true } } + + context "and customer_tags_match? returns true" do + before { expect(tag_rule).to receive(:customer_tags_match?) { true } } + + it "applies the rule" do + tag_rule.apply + expect(tag_rule).to have_received(:apply!) + end + end + + context "when customer_tags_match? returns false" do + before { expect(tag_rule).to receive(:customer_tags_match?) { false } } + before { allow(tag_rule).to receive(:apply_default!) } + + context "and the rule responds to #apply_default!" do + before { allow(tag_rule).to receive(:respond_to?).with(:apply_default!, true) { true } } + + it "applies the default action" do + tag_rule.apply + expect(tag_rule).to_not have_received(:apply!) + expect(tag_rule).to have_received(:apply_default!) + end + end + + context "and the rule does not respond to #apply_default!" do + before { allow(tag_rule).to receive(:respond_to?).with(:apply_default!, true) { false } } + + it "does not apply the rule or the default action" do + tag_rule.apply + expect(tag_rule).to_not have_received(:apply!) + expect(tag_rule).to_not have_received(:apply_default!) + end + end + end + end + + context "when the rule is deemed not to be relevant" do + before { allow(tag_rule).to receive(:relevant?) { false } } + + it "does not apply the rule" do + tag_rule.apply + expect(tag_rule).to_not have_received(:apply!) + end + end + end + end +end diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb index 9efc53b521..a24a921f43 100644 --- a/spec/models/variant_override_spec.rb +++ b/spec/models/variant_override_spec.rb @@ -30,6 +30,22 @@ describe VariantOverride do end + describe "callbacks" do + let!(:vo) { create(:variant_override, hub: hub, variant: variant) } + + it "refreshes the products cache on save" do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_override_changed).with(vo) + vo.price = 123.45 + vo.save + end + + it "refreshes the products cache on destroy" do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_override_destroyed).with(vo) + vo.destroy + end + end + + describe "looking up prices" do it "returns the numeric price when present" do VariantOverride.create!(variant: variant, hub: hub, price: 12.34) diff --git a/spec/serializers/enterprise_serializer_spec.rb b/spec/serializers/enterprise_serializer_spec.rb index f70852d973..f5eff4f771 100644 --- a/spec/serializers/enterprise_serializer_spec.rb +++ b/spec/serializers/enterprise_serializer_spec.rb @@ -1,4 +1,4 @@ -#require 'spec_helper' +require 'spec_helper' describe Api::EnterpriseSerializer do let(:serializer) { Api::EnterpriseSerializer.new enterprise, data: data } diff --git a/spec/serializers/order_serializer_spec.rb b/spec/serializers/order_serializer_spec.rb new file mode 100644 index 0000000000..2c327537eb --- /dev/null +++ b/spec/serializers/order_serializer_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Api::OrderSerializer do + let(:serializer) { Api::OrderSerializer.new order } + let(:order) { create(:completed_order_with_totals) } + + + it "serializes an order" do + expect(serializer.to_json).to match order.number.to_s + end + + it "convert the state attributes to translatable keys" do + expect(serializer.to_json).to match "complete" + expect(serializer.to_json).to match "balance_due" + end + +end diff --git a/spec/serializers/orders_by_distributor_serializer_spec.rb b/spec/serializers/orders_by_distributor_serializer_spec.rb new file mode 100644 index 0000000000..5976e9ad48 --- /dev/null +++ b/spec/serializers/orders_by_distributor_serializer_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Api::OrdersByDistributorSerializer do + + # Banged lets ensure entered into test database + let!(:distributor1) { create(:distributor_enterprise) } + let!(:distributor2) { create(:distributor_enterprise) } + let!(:user) { create(:user)} + let!(:d1o1) { create(:completed_order_with_totals, distributor: distributor1, user_id: user.id, total: 10000)} + let!(:d1o2) { create(:completed_order_with_totals, distributor: distributor1, user_id: user.id, total: 5000)} + let!(:d2o1) { create(:completed_order_with_totals, distributor: distributor2, user_id: user.id)} + + before do + @data = Enterprise.includes(:distributed_orders).where(enterprises: {id: user.enterprises_ordered_from }, spree_orders: {state: :complete, user_id: user.id}).to_a + @serializer = ActiveModel::ArraySerializer.new(@data, {each_serializer: Api::OrdersByDistributorSerializer}) + end + + it "serializes orders" do + expect(@serializer.to_json).to match "distributed_orders" + end + + it "serializes the balance for each distributor" do + expect(@serializer.serializable_array[0].keys).to include :balance + # Would be good to test adding up balance properly but can't get a non-zero total from the factories... + expect(@serializer.serializable_array[0][:balance]).to eq "0.00" + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 80d7c2eeb7..a477ad31e1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,7 @@ require 'rubygems' require 'pry' unless ENV['CI'] require 'knapsack' +Knapsack.tracker.config({enable_time_offset_warning: false}) unless ENV['CI'] Knapsack::Adapters::RSpecAdapter.bind ENV["RAILS_ENV"] ||= 'test'