From 28792fc8952fa7f44f7d10362c38aabd89896a00 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Thu, 26 Jul 2018 13:21:49 +0800 Subject: [PATCH 001/190] Add tests for adding/updating enterprise images --- .../admin/enterprises/form/_images.html.haml | 9 +- .../features/admin/enterprises/images_spec.rb | 82 +++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 spec/features/admin/enterprises/images_spec.rb diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml index 97d31dabcc..ea718607b5 100644 --- a/app/views/admin/enterprises/form/_images.html.haml +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -1,12 +1,13 @@ -.row +.row.page-admin-enterprises-form__logo-field-group.image-field-group .alpha.three.columns = f.label :logo %br 100 x 100 pixels .omega.eight.columns - = image_tag @object.logo(:medium) if @object.logo.present? + = image_tag @object.logo(:medium), class: 'image-field-group__preview-image' if @object.logo.present? = f.file_field :logo -.row + +.row.page-admin-enterprises-form__promo-image-field-group.image-field-group .alpha.three.columns = f.label :promo_image, 'ofn-with-tip' => t('.promo_image_placeholder') %br/ @@ -17,5 +18,5 @@ = t('.promo_image_note3') .omega.eight.columns - = image_tag @object.promo_image(:large) if @object.promo_image.present? + = image_tag @object.promo_image(:large), class: 'image-field-group__preview-image' if @object.promo_image.present? = f.file_field :promo_image diff --git a/spec/features/admin/enterprises/images_spec.rb b/spec/features/admin/enterprises/images_spec.rb new file mode 100644 index 0000000000..01f8754e2d --- /dev/null +++ b/spec/features/admin/enterprises/images_spec.rb @@ -0,0 +1,82 @@ +require "spec_helper" + +feature "Managing enterprise images" do + include AuthenticationWorkflow + include WebHelper + + context "as an Enterprise user", js: true do + let(:enterprise_user) { create_enterprise_user(enterprise_limit: 1) } + let(:distributor) { create(:distributor_enterprise, name: "First Distributor") } + + before do + enterprise_user.enterprise_roles.build(enterprise: distributor).save! + + quick_login_as enterprise_user + visit edit_admin_enterprise_path(distributor) + end + + describe "images for an enterprise", js: true do + def go_to_images + within(".side_menu") do + click_link "Images" + end + end + + before do + go_to_images + end + + scenario "editing logo" do + # Adding image + attach_file "enterprise[logo]", File.join(Rails.root, "app/assets/images/logo-white.png") + click_button "Update" + + expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + + go_to_images + within ".page-admin-enterprises-form__logo-field-group" do + expect(page).to have_selector(".image-field-group__preview-image") + expect(html).to include("logo-white.png") + end + + # Replacing image + attach_file "enterprise[logo]", File.join(Rails.root, "app/assets/images/logo-black.png") + click_button "Update" + + expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + + go_to_images + within ".page-admin-enterprises-form__logo-field-group" do + expect(page).to have_selector(".image-field-group__preview-image") + expect(html).to include("logo-black.png") + end + end + + scenario "editing promo image" do + # Adding image + attach_file "enterprise[promo_image]", File.join(Rails.root, "app/assets/images/logo-white.png") + click_button "Update" + + expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + + go_to_images + within ".page-admin-enterprises-form__promo-image-field-group" do + expect(page).to have_selector(".image-field-group__preview-image") + expect(html).to include("logo-white.jpg") + end + + # Replacing image + attach_file "enterprise[promo_image]", File.join(Rails.root, "app/assets/images/logo-black.png") + click_button "Update" + + expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + + go_to_images + within ".page-admin-enterprises-form__promo-image-field-group" do + expect(page).to have_selector(".image-field-group__preview-image") + expect(html).to include("logo-black.jpg") + end + end + end + end +end From 1df1ddcf66cf15848a114f1e8cdd674d8722aa91 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 27 Aug 2018 23:06:54 +0100 Subject: [PATCH 002/190] Add spec for saving product and variant simultaneously --- spec/features/admin/product_import_spec.rb | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/spec/features/admin/product_import_spec.rb b/spec/features/admin/product_import_spec.rb index af23ddf9bf..31477f1338 100644 --- a/spec/features/admin/product_import_spec.rb +++ b/spec/features/admin/product_import_spec.rb @@ -201,6 +201,44 @@ feature "Product Import", js: true do expect(Spree::Product.find_by_name('Beans').on_hand).to eq 0 end + it "can save a new product and variant of that product at the same time" do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "display_name"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "5", "3.50", "500", "g", "Small Bag"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "5.50", "2", "kg", "Big Bag"] + end + File.write('/tmp/test.csv', csv_data) + + visit main_app.admin_product_import_path + attach_file 'file', '/tmp/test.csv' + click_button 'Upload' + + import_data + + expect(page).to have_selector '.item-count', text: "2" + expect(page).to_not have_selector '.invalid-count' + expect(page).to have_selector '.create-count', text: "2" + expect(page).to_not have_selector '.update-count' + expect(page).to_not have_selector '.update-count' + expect(page).to_not have_selector '.inv-create-count' + expect(page).to_not have_selector '.inv-update-count' + + save_data + + small_bag = Spree::Variant.find_by_display_name('Small Bag') + expect(small_bag.product.name).to eq 'Potatoes' + expect(small_bag.price).to eq 3.50 + expect(small_bag.on_hand).to eq 5 + + big_bag = Spree::Variant.find_by_display_name('Big Bag') + expect(big_bag.product.name).to eq 'Potatoes' + expect(big_bag.price).to eq 5.50 + expect(big_bag.on_hand).to eq 6 + + expect(big_bag.product.id).to eq small_bag.product.id + end + + it "can import items into inventory" do csv_data = CSV.generate do |csv| csv << ["name", "supplier", "producer", "category", "on_hand", "price", "units"] From 8c2a49a57c7a173226da6141322453eb1013e822 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 27 Aug 2018 23:08:19 +0100 Subject: [PATCH 003/190] Update PI model spec --- spec/models/product_importer_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index c0cbc6718f..1714e0f88e 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -272,6 +272,8 @@ describe ProductImport::ProductImporter do expect(big_bag.product.name).to eq 'Potatoes' expect(big_bag.price).to eq 5.50 expect(big_bag.on_hand).to eq 6 + + expect(big_bag.product.id).to eq small_bag.product.id end end From 6b9b8d8b734be78acbdcb2671f06edc60566380d Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 28 Aug 2018 03:26:17 +0100 Subject: [PATCH 004/190] Match multiple product names --- app/models/product_import/entry_validator.rb | 21 ++++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 4f48c7745c..873e1478e0 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -124,16 +124,15 @@ module ProductImport end def inventory_validation(entry) - # Checks a potential inventory item corresponds to a valid variant - match = Spree::Product.where(supplier_id: entry.producer_id, name: entry.name, deleted_at: nil).first + products = Spree::Product.where(supplier_id: entry.producer_id, name: entry.name, deleted_at: nil) - if match.nil? + if products.empty? mark_as_invalid(entry, attribute: 'name', error: I18n.t('admin.product_import.model.no_product')) return end - match.variants.each do |existing_variant| - unit_scale = match.variant_unit_scale + products.map(&:variants).flatten.each do |existing_variant| + unit_scale = existing_variant.product.variant_unit_scale unscaled_units = entry.unscaled_units || 0 entry.unit_value = unscaled_units * unit_scale @@ -176,24 +175,20 @@ module ProductImport end def product_validation(entry) - # Find product with matching supplier and name - match = Spree::Product.where(supplier_id: entry.supplier_id, name: entry.name, deleted_at: nil).first + products = Spree::Product.where(supplier_id: entry.supplier_id, name: entry.name, deleted_at: nil) - # If no matching product was found, create a new product - if match.nil? + if products.empty? mark_as_new_product(entry) return end - # Otherwise, if a variant exists with matching display_name and unit_value, update it - match.variants.each do |existing_variant| + products.map(&:variants).flatten.each do |existing_variant| if entry_matches_existing_variant?(entry, existing_variant) && existing_variant.deleted_at.nil? return mark_as_existing_variant(entry, existing_variant) end end - # Otherwise, a variant with sufficiently matching attributes doesn't exist; create a new one - mark_as_new_variant(entry, match.id) + mark_as_new_variant(entry, products.first.id) end def mark_as_new_product(entry) From f905284f7a2323490d81b21add1bc3e7736229b6 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 28 Aug 2018 12:45:04 +0100 Subject: [PATCH 005/190] Add spec for edge case --- spec/models/product_importer_spec.rb | 56 ++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index 1714e0f88e..d227c40d7e 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -16,6 +16,7 @@ describe ProductImport::ProductImporter do let!(:category) { create(:taxon, name: 'Vegetables') } let!(:category2) { create(:taxon, name: 'Cake') } + let!(:category3) { create(:taxon, name: 'Cereal') } let!(:tax_category) { create(:tax_category) } let!(:tax_category2) { create(:tax_category) } let!(:shipping_category) { create(:shipping_category) } @@ -28,6 +29,13 @@ describe ProductImport::ProductImporter do let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500') } let!(:product6) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Beetroot', unit_value: '500', on_demand: true, variant_unit_scale: 1, variant_unit: 'weight') } let!(:product7) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Tomato', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight') } + + let!(:product8) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category3.id) } + let!(:product9) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category3.id) } + let!(:variant2) { create(:variant, product_id: product8.id, price: '4.50', on_hand: '100', unit_value: '500', display_name: 'Porridge Oats') } + let!(:variant3) { create(:variant, product_id: product8.id, price: '5.50', on_hand: '100', unit_value: '500', display_name: 'Rolled Oats') } + let!(:variant4) { create(:variant, product_id: product9.id, price: '6.50', on_hand: '100', unit_value: '500', display_name: 'Flaked Oats') } + let!(:variant_override) { create(:variant_override, variant_id: product4.variants.first.id, hub: enterprise2, count_on_hand: 42) } let!(:variant_override2) { create(:variant_override, variant_id: product5.variants.first.id, hub: enterprise, count_on_hand: 96) } @@ -196,7 +204,7 @@ describe ProductImport::ProductImporter do end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') - settings = {enterprise2.id.to_s => {'import_into' => 'product_list'}} + settings = {'import_into' => 'product_list'} @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) end after { File.delete('/tmp/test-m.csv') } @@ -242,7 +250,7 @@ describe ProductImport::ProductImporter do end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') - settings = {enterprise.id.to_s => {'import_into' => 'product_list'}} + settings = {'import_into' => 'product_list'} @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) end after { File.delete('/tmp/test-m.csv') } @@ -286,7 +294,7 @@ describe ProductImport::ProductImporter do end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') - settings = {enterprise3.id.to_s => {'import_into' => 'product_list'}} + settings = {'import_into' => 'product_list'} @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) end after { File.delete('/tmp/test-m.csv') } @@ -319,6 +327,46 @@ describe ProductImport::ProductImporter do end end + describe "when more than one product of the same name already exists with multiple variants each" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "display_name"] + csv << ["Oats", "User Enterprise", "Cereal", "50", "3.50", "500", "g", "Rolled Oats"] # Update + csv << ["Oats", "User Enterprise", "Cereal", "80", "3.75", "500", "g", "Flaked Oats"] # Update + csv << ["Oats", "User Enterprise", "Cereal", "60", "5.50", "500", "g", "Magic Oats"] # Add + csv << ["Oats", "User Enterprise", "Cereal", "70", "8.50", "500", "g", "French Oats"] # Add + csv << ["Oats", "User Enterprise", "Cereal", "70", "8.50", "500", "g", "Scottish Oats"] # Add + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {'import_into' => 'product_list'} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } + + it "validates entries" do + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 5 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_product', entries)).to eq 3 + expect(filter('update_product', entries)).to eq 2 + expect(filter('create_inventory', entries)).to eq 0 + expect(filter('update_inventory', entries)).to eq 0 + end + + it "saves and updates" do + @importer.save_entries + + expect(@importer.products_created_count).to eq 3 + expect(@importer.products_updated_count).to eq 2 + expect(@importer.inventory_created_count).to eq 0 + expect(@importer.inventory_updated_count).to eq 0 + expect(@importer.updated_ids.count).to eq 5 + end + end + describe "importing items into inventory" do before do csv_data = CSV.generate do |csv| @@ -484,7 +532,7 @@ describe ProductImport::ProductImporter do @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, updated_ids: updated_ids, enterprises_to_reset: [enterprise.id], settings: settings) @importer.reset_absent(updated_ids) - expect(@importer.products_reset_count).to eq 2 + expect(@importer.products_reset_count).to eq 7 expect(Spree::Product.find_by_name('Carrots').on_hand).to eq 5 # Present in file, added expect(Spree::Product.find_by_name('Beans').on_hand).to eq 6 # Present in file, updated From 8de0355dc7884b10372b22ccc49a566973fb15cb Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 28 Aug 2018 14:05:57 +0100 Subject: [PATCH 006/190] Add spec for multiple stages during import --- spec/models/product_importer_spec.rb | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index d227c40d7e..b587ad8d76 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -367,6 +367,75 @@ describe ProductImport::ProductImporter do end end + describe "when importer processes create and update across multiple stages" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "display_name"] + csv << ["Bag of Oats", "User Enterprise", "Cereal", "60", "5.50", "500", "g", "Magic Oats"] # Add + csv << ["Bag of Oats", "User Enterprise", "Cereal", "70", "8.50", "500", "g", "French Oats"] # Add + csv << ["Bag of Oats", "User Enterprise", "Cereal", "80", "9.50", "500", "g", "Organic Oats"] # Add + csv << ["Bag of Oats", "User Enterprise", "Cereal", "90", "7.50", "500", "g", "Scottish Oats"] # Add + csv << ["Bag of Oats", "User Enterprise", "Cereal", "30", "6.50", "500", "g", "Breakfast Oats"] # Add + end + File.write('/tmp/test-m.csv', csv_data) + @file = File.new('/tmp/test-m.csv') + @settings = {'import_into' => 'product_list'} + end + after { File.delete('/tmp/test-m.csv') } + + it "processes the validation in stages" do + # Using settings of start: 1, end: 2 to simulate import over multiple stages + @importer = ProductImport::ProductImporter.new(@file, admin, start: 1, end: 3, settings: @settings) + + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 3 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_product', entries)).to eq 3 + expect(filter('update_product', entries)).to eq 0 + expect(filter('create_inventory', entries)).to eq 0 + expect(filter('update_inventory', entries)).to eq 0 + + @importer = ProductImport::ProductImporter.new(@file, admin, start: 4, end: 6, settings: @settings) + + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 2 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_product', entries)).to eq 2 + expect(filter('update_product', entries)).to eq 0 + expect(filter('create_inventory', entries)).to eq 0 + expect(filter('update_inventory', entries)).to eq 0 + end + + it "processes saving in stages" do + @importer = ProductImport::ProductImporter.new(@file, admin, start: 1, end: 3, settings: @settings) + @importer.save_entries + + expect(@importer.products_created_count).to eq 3 + expect(@importer.products_updated_count).to eq 0 + expect(@importer.inventory_created_count).to eq 0 + expect(@importer.inventory_updated_count).to eq 0 + expect(@importer.updated_ids.count).to eq 3 + + @importer = ProductImport::ProductImporter.new(@file, admin, start: 4, end: 6, settings: @settings) + @importer.save_entries + + expect(@importer.products_created_count).to eq 2 + expect(@importer.products_updated_count).to eq 0 + expect(@importer.inventory_created_count).to eq 0 + expect(@importer.inventory_updated_count).to eq 0 + expect(@importer.updated_ids.count).to eq 2 + + products = Spree::Product.find_all_by_name('Bag of Oats') + + expect(products.count).to eq 1 + expect(products.first.variants.count).to eq 5 + end + end + describe "importing items into inventory" do before do csv_data = CSV.generate do |csv| From 1de13a504975bf8ea6472ec9f2b56047354ea576 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 30 Aug 2018 18:41:27 +0200 Subject: [PATCH 007/190] Favor #public_send over #send using Rubocop's cop Devs keep using `#send` although that method does not preserve private/protected visibility. Watching after this turned out to be quite time-consuming while doing code review. Currently, the Style/Send cop doesn't enforce `#public_send` however (that's what we want). It simply discourages the use of #send. See https://github.com/rubocop-hq/rubocop/pull/2081#issuecomment-292251650 for details. So a new entry on the Code Conventions doc has been added to overcome this limitation: https://github.com/openfoodfoundation/openfoodnetwork/wiki/Code-Conventions#prefer-public_send-over-send --- .rubocop.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 80d4c1bf7e..d46276275e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -33,6 +33,9 @@ Style/HashSyntax: Enabled: true EnforcedStyle: ruby19_no_mixed_keys +Style/Send: + Enabled: true + Layout/MultilineMethodCallIndentation: Enabled: true EnforcedStyle: indented From f5c44ef0e6c7bfe68730e5e33e22c0842f7a12f8 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Fri, 31 Aug 2018 15:15:50 +0800 Subject: [PATCH 008/190] Rename "entreprise" in i18n keys to "enterprise" --- .../side_menu_controller.js.coffee | 4 +- .../enterprise_groups/_form_address.html.haml | 16 +++--- .../enterprise_groups/_form_images.html.haml | 8 +-- .../_form_primary_details.html.haml | 4 +- .../enterprise_groups/_form_users.html.haml | 4 +- .../enterprise_groups/_form_web.html.haml | 4 +- .../admin/enterprise_groups/index.html.haml | 10 ++-- .../_enterprise_relationship.html.haml | 2 +- .../enterprise_relationships/_form.html.haml | 6 +-- .../_search_input.html.haml | 2 +- .../enterprise_relationships/index.html.haml | 2 +- .../reports/users_and_enterprises.html.haml | 2 +- config/locales/en.yml | 52 +++++++++---------- 13 files changed, 58 insertions(+), 58 deletions(-) diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee index f69c5365d5..1006ac7aa4 100644 --- a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee @@ -8,8 +8,8 @@ angular.module("admin.enterprise_groups") { name: 'users', label: t('users'), icon_class: "icon-user" } { name: 'about', label: t('about'), icon_class: "icon-pencil" } { name: 'images', label: t('images'), icon_class: "icon-picture" } - { name: 'contact', label: t('admin_entreprise_groups_contact'), icon_class: "icon-phone" } - { name: 'web', label: t('admin_entreprise_groups_web'), icon_class: "icon-globe" } + { name: 'contact', label: t('admin_enterprise_groups_contact'), icon_class: "icon-phone" } + { name: 'web', label: t('admin_enterprise_groups_web'), icon_class: "icon-globe" } ] $scope.select(0) diff --git a/app/views/admin/enterprise_groups/_form_address.html.haml b/app/views/admin/enterprise_groups/_form_address.html.haml index 2b8add5392..1e93c5f7f9 100644 --- a/app/views/admin/enterprise_groups/_form_address.html.haml +++ b/app/views/admin/enterprise_groups/_form_address.html.haml @@ -5,7 +5,7 @@ .alpha.three.columns = af.label :phone .omega.eight.columns - = af.text_field :phone, { placeholder: t(:admin_entreprise_groups_contact_phone_placeholder)} + = af.text_field :phone, { placeholder: t(:admin_enterprise_groups_contact_phone_placeholder)} .row .alpha.three.columns = f.label :email @@ -15,7 +15,7 @@ .three.columns.alpha = af.label :address1 .eight.columns.omega - = af.text_field :address1, { placeholder: t(:admin_entreprise_groups_contact_address1_placeholder)} + = af.text_field :address1, { placeholder: t(:admin_enterprise_groups_contact_address1_placeholder)} .row .alpha.three.columns = af.label :address2 @@ -23,17 +23,17 @@ = af.text_field :address2 .row .three.columns.alpha - = af.label :city, t(:admin_entreprise_groups_contact_city) + = af.label :city, t(:admin_enterprise_groups_contact_city) \/ - = af.label :zipcode, t(:admin_entreprise_groups_contact_zipcode) + = af.label :zipcode, t(:admin_enterprise_groups_contact_zipcode) .four.columns - = af.text_field :city, { placeholder: t(:admin_entreprise_groups_contact_city_placeholder)} + = af.text_field :city, { placeholder: t(:admin_enterprise_groups_contact_city_placeholder)} .four.columns.omega - = af.text_field :zipcode, { placeholder: t(:admin_entreprise_groups_contact_zipcode_placeholder)} + = af.text_field :zipcode, { placeholder: t(:admin_enterprise_groups_contact_zipcode_placeholder)} .row .three.columns.alpha - = af.label :state_id, t(:admin_entreprise_groups_contact_state_id) - = af.label :country_id, t(:admin_entreprise_groups_contact_country_id) + = af.label :state_id, t(:admin_enterprise_groups_contact_state_id) + = af.label :country_id, t(:admin_enterprise_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 7add7c914a..aa31913a20 100644 --- a/app/views/admin/enterprise_groups/_form_images.html.haml +++ b/app/views/admin/enterprise_groups/_form_images.html.haml @@ -2,16 +2,16 @@ %legend {{menu.selected.label}} .row .alpha.three.columns - = f.label :logo, 'ofn-with-tip' => t('admin_entreprise_groups_data_powertip_logo') - %div{'ofn-with-tip' => t('admin_entreprise_groups_data_powertip_logo')} + = f.label :logo, 'ofn-with-tip' => t('admin_enterprise_groups_data_powertip_logo') + %div{'ofn-with-tip' => t('admin_enterprise_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' => t(:admin_entreprise_groups_data_powertip_promo_image) - %div{'ofn-with-tip' => t('admin_entreprise_groups_data_powertip_promo_image')} + = f.label :promo_image, 'ofn-with-tip' => t(:admin_enterprise_groups_data_powertip_promo_image) + %div{'ofn-with-tip' => t('admin_enterprise_groups_data_powertip_promo_image')} %a= t 'admin.whats_this' .omega.eight.columns = image_tag @object.promo_image.url if @object.promo_image.present? 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 f003963b4e..ad40764fac 100644 --- a/app/views/admin/enterprise_groups/_form_primary_details.html.haml +++ b/app/views/admin/enterprise_groups/_form_primary_details.html.haml @@ -11,12 +11,12 @@ = f.text_field :description = f.field_container :on_front_page do - = f.label :on_front_page, t(:admin_entreprise_groups_on_front_page) + = f.label :on_front_page, t(:admin_enterprise_groups_on_front_page) %br/ = f.check_box :on_front_page = f.field_container :enterprise_ids do - = f.label :enterprise_ids, t(:admin_entreprise_groups_entreprise) + = f.label :enterprise_ids, t(:admin_enterprise_groups_enterprise) %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 22ac7921ca..f96212064e 100644 --- a/app/views/admin/enterprise_groups/_form_users.html.haml +++ b/app/views/admin/enterprise_groups/_form_users.html.haml @@ -2,8 +2,8 @@ %legend {{menu.selected.label}} .row .three.columns.alpha - =f.label :owner_id, t(:admin_entreprise_groups_owner) - .with-tip{'data-powertip' => t(:admin_entreprise_groups_data_powertip)} + =f.label :owner_id, t(:admin_enterprise_groups_owner) + .with-tip{'data-powertip' => t(:admin_enterprise_groups_data_powertip)} %a = t 'admin.whats_this' .eight.columns.omega diff --git a/app/views/admin/enterprise_groups/_form_web.html.haml b/app/views/admin/enterprise_groups/_form_web.html.haml index e0de2f7159..ad155ac95c 100644 --- a/app/views/admin/enterprise_groups/_form_web.html.haml +++ b/app/views/admin/enterprise_groups/_form_web.html.haml @@ -4,7 +4,7 @@ .alpha.three.columns = f.label :website .omega.eight.columns - = f.text_field :website, { placeholder: t(:admin_entreprise_groups_web_website_placeholder)} + = f.text_field :website, { placeholder: t(:admin_enterprise_groups_web_website_placeholder)} .row .alpha.three.columns = f.label :facebook, 'Facebook' @@ -24,4 +24,4 @@ .alpha.three.columns = f.label :twitter .omega.eight.columns - = f.text_field :twitter, { placeholder: t(:admin_entreprise_groups_web_twitter) } + = f.text_field :twitter, { placeholder: t(:admin_enterprise_groups_web_twitter) } diff --git a/app/views/admin/enterprise_groups/index.html.haml b/app/views/admin/enterprise_groups/index.html.haml index 2540c64980..93d7ee81ba 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 - = t 'admin_entreprise_groups' + = t 'admin_enterprise_groups' - if admin_user? = content_for :page_actions do @@ -9,14 +9,14 @@ %thead %tr %th - = t 'admin_entreprise_groups_name' + = t 'admin_enterprise_groups_name' - if spree_current_user.admin? %th - = t 'admin_entreprise_groups_owner' + = t 'admin_enterprise_groups_owner' %th - = t 'admin_entreprise_groups_on_front_page' + = t 'admin_enterprise_groups_on_front_page' %th - = t 'admin_entreprise_groups_entreprise' + = t 'admin_enterprise_groups_enterprise' %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 05e6505868..c1c6ccbc4b 100644 --- a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml +++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml @@ -1,7 +1,7 @@ %tr{"ng-repeat" => "enterprise_relationship in EnterpriseRelationships.enterprise_relationships | keywords:query"} %td {{ enterprise_relationship.parent_name }} %td - = t 'admin_entreprise_relationships_permits' + = t 'admin_enterprise_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 4d68d61e1b..958ce53074 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 - = t 'admin_entreprise_relationships_permits' + = t 'admin_enterprise_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()"}} - = t 'admin_entreprise_relationships_everything' + = t 'admin_enterprise_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: t(:admin_entreprise_relationships_button_create), "ng-click" => "create()"} + %input{type: "button", value: t(:admin_enterprise_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 350089dc4f..a85f201b91 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" => t(:admin_entreprise_relationships_seach_placeholder)} +%input.search{"ng-model" => "query", "placeholder" => t(:admin_enterprise_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 bdff595f09..c9a12531f0 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 - = t 'admin_entreprise_relationships' + = t 'admin_enterprise_relationships' = render 'admin/shared/enterprises_sub_menu' 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 d7a521911f..4c74a79e0d 100644 --- a/app/views/spree/admin/reports/users_and_enterprises.html.haml +++ b/app/views/spree/admin/reports/users_and_enterprises.html.haml @@ -1,6 +1,6 @@ = form_tag spree.users_and_enterprises_admin_reports_url do |f| .row - .alpha.two.columns= label_tag nil, t(:report_entreprises) + .alpha.two.columns= label_tag nil, t(:report_enterprises) .omega.fourteen.columns= select_tag(:enterprise_id_in, options_from_collection_for_select(Enterprise.all, :id, :name, params[:enterprise_id_in].andand.split(",")), {class: "select2 fullwidth", multiple: true}) .row diff --git a/config/locales/en.yml b/config/locales/en.yml index 14eb3ebaf7..161e240f13 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1995,31 +1995,31 @@ See the %{link} to find out more about %{sitename}'s features and to start using you_have_no_orders_yet: "You have no orders yet" running_balance: "Running balance" outstanding_balance: "Outstanding balance" - admin_entreprise_relationships: "Enterprise Permissions" - 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_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_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_enterprise_relationships: "Enterprise Permissions" + admin_enterprise_relationships_everything: "Everything" + admin_enterprise_relationships_permits: "permits" + admin_enterprise_relationships_seach_placeholder: "Search" + admin_enterprise_relationships_button_create: "Create" + admin_enterprise_groups: "Enterprise Groups" + admin_enterprise_groups_name: "Name" + admin_enterprise_groups_owner: "Owner" + admin_enterprise_groups_on_front_page: "On front page ?" + admin_enterprise_groups_enterprise: "Enterprises" + admin_enterprise_groups_data_powertip: "The primary user responsible for this group." + admin_enterprise_groups_data_powertip_logo: "This is the logo for the group" + admin_enterprise_groups_data_powertip_promo_image: "This image is displayed at the top of the Group profile" + admin_enterprise_groups_contact: "Contact" + admin_enterprise_groups_contact_phone_placeholder: "eg. 98 7654 3210" + admin_enterprise_groups_contact_address1_placeholder: "eg. 123 High Street" + admin_enterprise_groups_contact_city: "Suburb" + admin_enterprise_groups_contact_city_placeholder: "eg. Northcote" + admin_enterprise_groups_contact_zipcode: "Postcode" + admin_enterprise_groups_contact_zipcode_placeholder: "eg. 3070" + admin_enterprise_groups_contact_state_id: "State" + admin_enterprise_groups_contact_country_id: "Country" + admin_enterprise_groups_web: "Web Resources" + admin_enterprise_groups_web_twitter: "eg. @the_prof" + admin_enterprise_groups_web_website_placeholder: "eg. www.truffles.com" admin_order_cycles: "Admin Order Cycles" open: "Open" close: "Close" @@ -2157,7 +2157,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using report_payment_totals: 'Payment Totals' report_all: 'all' report_order_cycle: "Order Cycle: " - report_entreprises: "Enterprises: " + report_enterprises: "Enterprises: " report_users: "Users: " report_tax_rates: Tax rates report_tax_types: Tax types From 04d50d455502405f59092ca4b0c4360ab2dce056 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Fri, 31 Aug 2018 15:00:29 +0100 Subject: [PATCH 009/190] Fix filter results bug in validation section --- .../admin/product_import/filters/filter_entries.js.coffee | 2 +- spec/features/admin/product_import_spec.rb | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/product_import/filters/filter_entries.js.coffee b/app/assets/javascripts/admin/product_import/filters/filter_entries.js.coffee index 4a1007b3a9..efbcf5652b 100644 --- a/app/assets/javascripts/admin/product_import/filters/filter_entries.js.coffee +++ b/app/assets/javascripts/admin/product_import/filters/filter_entries.js.coffee @@ -10,7 +10,7 @@ angular.module("admin.productImport").filter 'entriesFilterValid', -> if type == 'valid' and validates_as != '' \ or type == 'invalid' and validates_as == '' \ - or type == 'create_product' and validates_as == 'new_product' or validates_as == 'new_variant' \ + or type == 'create_product' and (validates_as == 'new_product' or validates_as == 'new_variant') \ or type == 'update_product' and validates_as == 'existing_variant' \ or type == 'create_inventory' and validates_as == 'new_inventory_item' \ or type == 'update_inventory' and validates_as == 'existing_inventory_item' diff --git a/spec/features/admin/product_import_spec.rb b/spec/features/admin/product_import_spec.rb index 31477f1338..be6c01810c 100644 --- a/spec/features/admin/product_import_spec.rb +++ b/spec/features/admin/product_import_spec.rb @@ -201,11 +201,12 @@ feature "Product Import", js: true do expect(Spree::Product.find_by_name('Beans').on_hand).to eq 0 end - it "can save a new product and variant of that product at the same time" do + it "can save a new product and variant of that product at the same time, add variant to existing product" do csv_data = CSV.generate do |csv| csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "display_name"] csv << ["Potatoes", "User Enterprise", "Vegetables", "5", "3.50", "500", "g", "Small Bag"] csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "5.50", "2", "kg", "Big Bag"] + csv << ["Beans", "User Enterprise", "Vegetables", "7", "2.50", "250", "g", nil] end File.write('/tmp/test.csv', csv_data) @@ -215,9 +216,9 @@ feature "Product Import", js: true do import_data - expect(page).to have_selector '.item-count', text: "2" + expect(page).to have_selector '.item-count', text: "3" expect(page).to_not have_selector '.invalid-count' - expect(page).to have_selector '.create-count', text: "2" + expect(page).to have_selector '.create-count', text: "3" expect(page).to_not have_selector '.update-count' expect(page).to_not have_selector '.update-count' expect(page).to_not have_selector '.inv-create-count' From 75a9ea5bfa79431bf039be30d3a942c0f7c849b1 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Fri, 31 Aug 2018 15:38:38 +0100 Subject: [PATCH 010/190] Prefer .flat_map(&foo) over .map(&foo).flatten --- app/models/product_import/entry_validator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 873e1478e0..33a7932f4f 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -131,7 +131,7 @@ module ProductImport return end - products.map(&:variants).flatten.each do |existing_variant| + products.flat_map(&:variants).each do |existing_variant| unit_scale = existing_variant.product.variant_unit_scale unscaled_units = entry.unscaled_units || 0 entry.unit_value = unscaled_units * unit_scale @@ -182,7 +182,7 @@ module ProductImport return end - products.map(&:variants).flatten.each do |existing_variant| + products.flat_map(&:variants).each do |existing_variant| if entry_matches_existing_variant?(entry, existing_variant) && existing_variant.deleted_at.nil? return mark_as_existing_variant(entry, existing_variant) end From 4557bfbc178c9596d44e03f28ca87cceb081348e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 31 Aug 2018 17:49:12 +0000 Subject: [PATCH 011/190] [Security] Bump rubyzip from 1.2.1 to 1.2.2 Bumps [rubyzip](https://github.com/rubyzip/rubyzip) from 1.2.1 to 1.2.2. **This update includes security fixes.** - [Release notes](https://github.com/rubyzip/rubyzip/releases) - [Changelog](https://github.com/rubyzip/rubyzip/blob/master/Changelog.md) - [Commits](https://github.com/rubyzip/rubyzip/compare/v1.2.1...v1.2.2) Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index b8644adb02..b66ed564fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -630,7 +630,7 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-ole (1.2.12.1) ruby-progressbar (1.10.0) - rubyzip (1.2.1) + rubyzip (1.2.2) safe_yaml (1.0.4) sass (3.3.14) sass-rails (3.2.6) From 4dfbbd60d44a700d1fd2ebc75861b57d6559fd4b Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Fri, 31 Aug 2018 19:13:17 +0100 Subject: [PATCH 012/190] Fix occasional StaleObjectError on variant updates --- app/models/product_import/entry_processor.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 97f9cbe55c..14cfd67f2d 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -115,7 +115,13 @@ module ProductImport return unless entry.validates_as? 'existing_variant' - save_variant entry + begin + save_variant entry + rescue ActiveRecord::StaleObjectError + entry.product_object.reload + save_variant entry + end + @variants_updated += 1 end From d8bbcdc54bbc8c71327dc86f05daf248b8b2a95a Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Fri, 31 Aug 2018 22:52:44 +0100 Subject: [PATCH 013/190] Decrease batch size to reduce chance of timeouts --- .../product_import/controllers/import_form_controller.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/product_import/controllers/import_form_controller.js.coffee b/app/assets/javascripts/admin/product_import/controllers/import_form_controller.js.coffee index 75b506fc66..d9b73cdabf 100644 --- a/app/assets/javascripts/admin/product_import/controllers/import_form_controller.js.coffee +++ b/app/assets/javascripts/admin/product_import/controllers/import_form_controller.js.coffee @@ -51,7 +51,7 @@ angular.module("admin.productImport").controller "ImportFormCtrl", ($scope, $htt $scope.start = () -> $scope.started = true total = ams_data.item_count - size = 100 + size = 50 $scope.chunks = Math.ceil(total / size) i = 0 From 9d05e5c97a4f5601bc0e8420735d48c36a31585b Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Sat, 1 Sep 2018 14:09:07 +0100 Subject: [PATCH 014/190] Remove duplicate line in spec --- spec/features/admin/product_import_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/admin/product_import_spec.rb b/spec/features/admin/product_import_spec.rb index be6c01810c..8043ab82a0 100644 --- a/spec/features/admin/product_import_spec.rb +++ b/spec/features/admin/product_import_spec.rb @@ -220,7 +220,6 @@ feature "Product Import", js: true do expect(page).to_not have_selector '.invalid-count' expect(page).to have_selector '.create-count', text: "3" expect(page).to_not have_selector '.update-count' - expect(page).to_not have_selector '.update-count' expect(page).to_not have_selector '.inv-create-count' expect(page).to_not have_selector '.inv-update-count' From e62a755ddbd19bed7a4eb45810ea45a4637d84f4 Mon Sep 17 00:00:00 2001 From: apoc64 Date: Sun, 2 Sep 2018 11:08:27 -0600 Subject: [PATCH 015/190] Adds comments for setting up timezone in application.yml --- config/application.yml.example | 1 + 1 file changed, 1 insertion(+) diff --git a/config/application.yml.example b/config/application.yml.example index 980d3f9b26..ab5810cc4a 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -4,6 +4,7 @@ # Minimum 30 but usually 128 characters. To obtain run 'rake secret', or faster, 'openssl rand -hex 128' SECRET_TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# Time zone must match the operating system time zone in order to pass all tests. This is a string which is the key for the MAPPING hash constant in the ActiveSupport::TimeZone class. Documentation for this class including the entire hash can be found here: https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html TIMEZONE: Melbourne # Default country for dropdowns etc. See for codes: http://en.wikipedia.org/wiki/ISO_3166-1 DEFAULT_COUNTRY_CODE: AU From f74a6e47d5fd4cd4abc3ef5d162bb6d144890e98 Mon Sep 17 00:00:00 2001 From: VadLusk Date: Mon, 3 Sep 2018 09:20:26 +1000 Subject: [PATCH 016/190] Remove css_splitter references, delete all_split2.css. #1361 #2633 --- Gemfile | 1 - Gemfile.lock | 5 +---- app/assets/stylesheets/darkswarm/all_split2.css | 3 --- app/controllers/application_controller.rb | 1 - app/views/layouts/darkswarm.html.haml | 2 +- 5 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 app/assets/stylesheets/darkswarm/all_split2.css diff --git a/Gemfile b/Gemfile index 1147d4c59c..04f240a002 100644 --- a/Gemfile +++ b/Gemfile @@ -103,7 +103,6 @@ gem 'foundation_rails_helper', github: 'willrjmarshall/foundation_rails_helper', gem 'jquery-rails' gem 'jquery-migrate-rails' -gem 'css_splitter' gem 'ofn-qz', github: 'openfoodfoundation/ofn-qz', ref: '60da2ae4c44cbb4c8d602f59fb5fff8d0f21db3c' diff --git a/Gemfile.lock b/Gemfile.lock index b8644adb02..98534f621b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -243,9 +243,7 @@ GEM safe_yaml (~> 1.0.0) css_parser (1.3.5) addressable - css_splitter (0.4.5) - sprockets (>= 2.0.0) - daemons (1.2.6) + daemons (1.2.2) dalli (2.7.2) database_cleaner (0.7.1) db2fog (0.8.0) @@ -725,7 +723,6 @@ DEPENDENCIES capybara (>= 2.15.4) coffee-rails (~> 3.2.1) compass-rails - css_splitter custom_error_message! daemons dalli diff --git a/app/assets/stylesheets/darkswarm/all_split2.css b/app/assets/stylesheets/darkswarm/all_split2.css deleted file mode 100644 index e231ca5d02..0000000000 --- a/app/assets/stylesheets/darkswarm/all_split2.css +++ /dev/null @@ -1,3 +0,0 @@ -/* - *= require 'darkswarm/all' - */ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 65d1fb1f33..4b0cae8049 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,7 +7,6 @@ class ApplicationController < ActionController::Base before_filter :set_cache_headers # Issue #1213, prevent cart emptying via cache when using back button include EnterprisesHelper - helper CssSplitter::ApplicationHelper def redirect_to(options = {}, response_status = {}) ::Rails.logger.error("Redirected by #{caller(1).first rescue "unknown"}") diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 2b796ad0cc..e3cd92951f 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -17,7 +17,7 @@ = yield :scripts %script{:src => "https://js.stripe.com/v3/", :type => "text/javascript"} %script{src: "//maps.googleapis.com/maps/api/js?libraries=places,geometry#{ ENV['GOOGLE_MAPS_API_KEY'] ? '&key=' + ENV['GOOGLE_MAPS_API_KEY'] : ''} "} - = split_stylesheet_link_tag "darkswarm/all" + = stylesheet_link_tag "darkswarm/all" = javascript_include_tag "darkswarm/all" = render "layouts/i18n_script" From 14d526efc2a76d477cd4dde9c7d875df9aae297a Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Sat, 11 Aug 2018 23:31:27 +0100 Subject: [PATCH 017/190] removed assets related to spree store: dead code --- app/assets/javascripts/store/all.js | 12 ------------ app/assets/stylesheets/store/all.css | 12 ------------ config/application.rb | 3 +-- 3 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 app/assets/javascripts/store/all.js delete mode 100644 app/assets/stylesheets/store/all.css diff --git a/app/assets/javascripts/store/all.js b/app/assets/javascripts/store/all.js deleted file mode 100644 index a8386e4b7f..0000000000 --- a/app/assets/javascripts/store/all.js +++ /dev/null @@ -1,12 +0,0 @@ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically -// be included in the compiled file accessible from http://example.com/assets/application.js -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// the compiled file. -// - -//= require 'jquery' -//= require store/spree_frontend -//= require store/spree_auth - -//= require_tree . diff --git a/app/assets/stylesheets/store/all.css b/app/assets/stylesheets/store/all.css deleted file mode 100644 index ea25eaeefc..0000000000 --- a/app/assets/stylesheets/store/all.css +++ /dev/null @@ -1,12 +0,0 @@ -/* - * This is a manifest file that'll automatically include all the stylesheets available in this directory - * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at - * the top of the compiled file, but it's generally better to create a new file per style scope. - * - - *= require store/spree_frontend - *= require store/spree_auth - - *= require_self - *= require_tree . -*/ diff --git a/config/application.rb b/config/application.rb index ea3539556b..5e1c31e6b8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -141,11 +141,10 @@ module Openfoodnetwork # Instead, they must be explicitly included below # http://stackoverflow.com/questions/8012434/what-is-the-purpose-of-config-assets-precompile config.assets.initialize_on_precompile = true - config.assets.precompile += ['store/all.css', 'store/all.js', 'store/shop_front.js', 'iehack.js'] + config.assets.precompile += ['iehack.js'] config.assets.precompile += ['admin/all.css', 'admin/*.js', 'admin/**/*.js'] config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all_split2.css', 'darkswarm/all.js'] config.assets.precompile += ['mail/all.css'] - config.assets.precompile += ['search/all.css', 'search/*.js'] config.assets.precompile += ['shared/*'] config.assets.precompile += ['qz/*'] From fbce828305d848fd0fa6224a77f0b4d20ce263f5 Mon Sep 17 00:00:00 2001 From: apoc64 Date: Mon, 3 Sep 2018 08:56:42 -0600 Subject: [PATCH 018/190] Breaks comment into multiple lines --- config/application.yml.example | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/application.yml.example b/config/application.yml.example index ab5810cc4a..27710fbbc4 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -4,7 +4,9 @@ # Minimum 30 but usually 128 characters. To obtain run 'rake secret', or faster, 'openssl rand -hex 128' SECRET_TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -# Time zone must match the operating system time zone in order to pass all tests. This is a string which is the key for the MAPPING hash constant in the ActiveSupport::TimeZone class. Documentation for this class including the entire hash can be found here: https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html +# Time zone must match the operating system time zone in order to pass all tests. +# This string is the key for the MAPPING hash constant in ActiveSupport::TimeZone. +# Documentation including the hash: https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html TIMEZONE: Melbourne # Default country for dropdowns etc. See for codes: http://en.wikipedia.org/wiki/ISO_3166-1 DEFAULT_COUNTRY_CODE: AU From 8efbe0e6d248e0b54b48504c7be9147cb9ec3f33 Mon Sep 17 00:00:00 2001 From: VadLusk Date: Mon, 3 Sep 2018 11:08:38 -0600 Subject: [PATCH 019/190] Delete _split2.css from application.rb assets precompile array. --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index ea3539556b..33ad7f6011 100644 --- a/config/application.rb +++ b/config/application.rb @@ -143,7 +143,7 @@ module Openfoodnetwork config.assets.initialize_on_precompile = true config.assets.precompile += ['store/all.css', 'store/all.js', 'store/shop_front.js', 'iehack.js'] config.assets.precompile += ['admin/all.css', 'admin/*.js', 'admin/**/*.js'] - config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all_split2.css', 'darkswarm/all.js'] + config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all.js'] config.assets.precompile += ['mail/all.css'] config.assets.precompile += ['search/all.css', 'search/*.js'] config.assets.precompile += ['shared/*'] From f30032eee7f8d64e7444df15a33760b6da98596f Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sun, 29 Jul 2018 15:51:24 +0800 Subject: [PATCH 020/190] Include image URLs in serialized enterprise for admin --- .../api/admin/enterprise_serializer.rb | 29 ++++++++++ .../admin/enterprise_serializer_spec.rb | 54 ++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index e29954992f..348bb75461 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -5,11 +5,20 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :preferred_product_selection_from_inventory_only attributes :owner, :contact, :users, :tag_groups, :default_tag_group attributes :require_login, :allow_guest_orders, :allow_order_changes + attributes :logo, :promo_image has_one :owner, serializer: Api::Admin::UserSerializer has_many :users, serializer: Api::Admin::UserSerializer has_one :address, serializer: Api::AddressSerializer + def logo + attachment_urls(object.logo, [:thumb, :small, :medium]) + end + + def promo_image + attachment_urls(object.promo_image, [:thumb, :medium, :large]) + end + def tag_groups object.tag_rules.prioritised.reject(&:is_default).each_with_object([]) do |tag_rule, tag_groups| tag_group = find_match(tag_groups, tag_rule.preferred_customer_tags.split(",").map{ |t| { text: t } }) @@ -33,4 +42,24 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer end return { tags: tags, rules: [] } end + + private + + # Returns a hash of URLs for specified versions of an attachment. + # + # Example: + # + # attachment_urls(object.logo, [:thumb, :small, :medium]) + # # { + # # thumb: LOGO_THUMB_URL, + # # small: LOGO_SMALL_URL, + # # medium: LOGO_MEDIUM_URL + # # } + def attachment_urls(attachment, versions) + return unless attachment.exists? + + versions.each_with_object({}) do |version, urls| + urls[version] = attachment.url(version) + end + end end diff --git a/spec/serializers/admin/enterprise_serializer_spec.rb b/spec/serializers/admin/enterprise_serializer_spec.rb index 0898b7d4a0..5bf40559d0 100644 --- a/spec/serializers/admin/enterprise_serializer_spec.rb +++ b/spec/serializers/admin/enterprise_serializer_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe Api::Admin::EnterpriseSerializer do let(:enterprise) { create(:distributor_enterprise) } @@ -6,4 +6,56 @@ describe Api::Admin::EnterpriseSerializer do serializer = Api::Admin::EnterpriseSerializer.new enterprise serializer.to_json.should match enterprise.name end + + context "for logo" do + let(:enterprise) { create(:distributor_enterprise, logo: image) } + + context "when there is a logo" do + let(:image) do + image_path = File.open(Rails.root.join("app", "assets", "images", "logo-black.png")) + Rack::Test::UploadedFile.new(image_path, "image/png") + end + + it "includes URLs of image versions" do + serializer = Api::Admin::EnterpriseSerializer.new(enterprise) + expect(serializer.as_json[:logo]).to_not be_blank + expect(serializer.as_json[:logo][:medium]).to match(/logo-black.png/) + end + end + + context "when there is no logo" do + let(:image) { nil } + + it "includes URLs of image versions" do + serializer = Api::Admin::EnterpriseSerializer.new(enterprise) + expect(serializer.as_json[:logo]).to be_blank + end + end + end + + context "for promo image" do + let(:enterprise) { create(:distributor_enterprise, promo_image: image) } + + context "when there is a promo image" do + let(:image) do + image_path = File.open(Rails.root.join("app", "assets", "images", "logo-black.png")) + Rack::Test::UploadedFile.new(image_path, "image/png") + end + + it "includes URLs of image versions" do + serializer = Api::Admin::EnterpriseSerializer.new(enterprise) + expect(serializer.as_json[:promo_image]).to_not be_blank + expect(serializer.as_json[:promo_image][:medium]).to match(/logo-black.jpg/) + end + end + + context "when there is no promo image" do + let(:image) { nil } + + it "includes URLs of image versions" do + serializer = Api::Admin::EnterpriseSerializer.new(enterprise) + expect(serializer.as_json[:promo_image]).to be_nil + end + end + end end From 01d4b8fb1037facf0cc0f0c3d245a3cd34241422 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sun, 26 Aug 2018 03:41:54 +0800 Subject: [PATCH 021/190] Allow custom resource permission for admin controllers --- app/controllers/spree/admin/base_controller_decorator.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index fb0e54baa8..60887f2ce5 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -28,7 +28,11 @@ Spree::Admin::BaseController.class_eval do record = self.class.to_s.sub("Controller", "").underscore.split('/').last.singularize.to_sym end authorize! :admin, record - authorize! action, record + authorize! resource_authorize_action, record + end + + def resource_authorize_action + action end # This is in Spree::Core::ControllerHelpers::Auth From 9c3bb863da166cdc31c5e56b930762f586a46fb9 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Thu, 12 Jul 2018 11:43:10 +0800 Subject: [PATCH 022/190] Add endpoints for removing enterprise images --- app/controllers/api/base_controller.rb | 4 + .../api/enterprise_attachment_controller.rb | 37 ++++++++ app/controllers/api/logos_controller.rb | 16 ++++ .../api/promo_images_controller.rb | 16 ++++ app/models/spree/ability_decorator.rb | 2 +- config/locales/en.yml | 8 ++ config/routes.rb | 4 + spec/controllers/api/logos_controller_spec.rb | 90 +++++++++++++++++++ .../api/promo_images_controller_spec.rb | 90 +++++++++++++++++++ spec/models/spree/ability_spec.rb | 4 +- 10 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 app/controllers/api/enterprise_attachment_controller.rb create mode 100644 app/controllers/api/logos_controller.rb create mode 100644 app/controllers/api/promo_images_controller.rb create mode 100644 spec/controllers/api/logos_controller_spec.rb create mode 100644 spec/controllers/api/promo_images_controller_spec.rb diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index f25c47417d..a7f96d01cf 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -9,5 +9,9 @@ module Api include ActionController::UrlFor include Rails.application.routes.url_helpers use_renderers :json + + def respond_with_conflict(json_hash) + render json: json_hash, status: :conflict + end end end diff --git a/app/controllers/api/enterprise_attachment_controller.rb b/app/controllers/api/enterprise_attachment_controller.rb new file mode 100644 index 0000000000..083d6946be --- /dev/null +++ b/app/controllers/api/enterprise_attachment_controller.rb @@ -0,0 +1,37 @@ +module Api + class EnterpriseAttachmentController < BaseController + class MissingImplementationError < StandardError; end + class UnknownEnterpriseAuthorizationActionError < StandardError; end + + before_filter :load_enterprise + + respond_to :json + + def destroy + return respond_with_conflict(error: destroy_attachment_does_not_exist_error_message) unless @enterprise.public_send("#{attachment_name}?") + + @enterprise.update_attributes!(attachment_name => nil) + render json: @enterprise, serializer: Admin::EnterpriseSerializer, spree_current_user: spree_current_user + end + + protected + + def attachment_name + raise MissingImplementationError, "Method attachment_name should be defined" + end + + def enterprise_authorize_action + raise MissingImplementationError, "Method enterprise_authorize_action should be defined" + end + + def load_enterprise + @enterprise = Enterprise.find_by_permalink(params[:enterprise_id].to_s) + raise UnknownEnterpriseAuthorizationActionError if enterprise_authorize_action.blank? + authorize!(enterprise_authorize_action, @enterprise) + end + + def destroy_attachment_does_not_exist_error_message + I18n.t("api.enterprise_#{attachment_name}.destroy_attachment_does_not_exist") + end + end +end diff --git a/app/controllers/api/logos_controller.rb b/app/controllers/api/logos_controller.rb new file mode 100644 index 0000000000..f3e7934724 --- /dev/null +++ b/app/controllers/api/logos_controller.rb @@ -0,0 +1,16 @@ +module Api + class LogosController < EnterpriseAttachmentController + private + + def attachment_name + :logo + end + + def enterprise_authorize_action + case action_name.to_sym + when :destroy + :remove_logo + end + end + end +end diff --git a/app/controllers/api/promo_images_controller.rb b/app/controllers/api/promo_images_controller.rb new file mode 100644 index 0000000000..0e79ebdb93 --- /dev/null +++ b/app/controllers/api/promo_images_controller.rb @@ -0,0 +1,16 @@ +module Api + class PromoImagesController < EnterpriseAttachmentController + private + + def attachment_name + :promo_image + end + + def enterprise_authorize_action + case action_name.to_sym + when :destroy + :remove_promo_image + end + end + end +end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 53198251b9..9d0941a220 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -99,7 +99,7 @@ class AbilityDecorator end can [:admin, :index, :create], Enterprise - can [:read, :edit, :update, :bulk_update, :resend_confirmation], Enterprise do |enterprise| + can [:read, :edit, :update, :remove_logo, :remove_promo_image, :bulk_update, :resend_confirmation], Enterprise do |enterprise| OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise end can [:welcome, :register], Enterprise do |enterprise| diff --git a/config/locales/en.yml b/config/locales/en.yml index 540d306a95..61f74ad2a8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1077,6 +1077,14 @@ en: stripe_connect_settings: resource: Stripe Connect configuration +# API +# + api: + enterprise_logo: + destroy_attachment_does_not_exist: "Logo does not exist" + enterprise_promo_image: + destroy_attachment_does_not_exist: "Promo image does not exist" + # Frontend views # # These keys are referenced relatively like `t('.message')` in diff --git a/config/routes.rb b/config/routes.rb index 574140bd5b..4f6125c8ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -95,7 +95,11 @@ Openfoodnetwork::Application.routes.draw do post :update_image, on: :member get :managed, on: :collection get :accessible, on: :collection + + resource :logo, only: [:destroy] + resource :promo_image, only: [:destroy] end + resources :order_cycles do get :managed, on: :collection get :accessible, on: :collection diff --git a/spec/controllers/api/logos_controller_spec.rb b/spec/controllers/api/logos_controller_spec.rb new file mode 100644 index 0000000000..213ac8bc6e --- /dev/null +++ b/spec/controllers/api/logos_controller_spec.rb @@ -0,0 +1,90 @@ +require "spec_helper" + +module Api + describe LogosController, type: :controller do + include AuthenticationWorkflow + + let(:admin_user) { create(:admin_user) } + let(:enterprise_owner) { create(:user) } + let(:enterprise) { create(:enterprise, owner: enterprise_owner ) } + let(:enterprise_manager) { create(:user, enterprise_limit: 10, enterprises: [enterprise]) } + let(:other_enterprise_owner) { create(:user) } + let(:other_enterprise) { create(:enterprise, owner: other_enterprise_owner ) } + let(:other_enterprise_manager) { create(:user, enterprise_limit: 10, enterprises: [other_enterprise]) } + + describe "removing logo" do + image_path = File.open(Rails.root.join("app", "assets", "images", "logo-black.png")) + let(:image) { Rack::Test::UploadedFile.new(image_path, "image/png") } + + let(:enterprise) { create(:enterprise, owner: enterprise_owner, logo: image) } + + before do + allow(controller).to receive(:spree_current_user) { current_user } + end + + context "as manager" do + let(:current_user) { enterprise_manager } + + it "removes logo" do + spree_delete :destroy, enterprise_id: enterprise + + expect(response).to be_success + expect(json_response["id"]).to eq enterprise.id + enterprise.reload + expect(enterprise.logo?).to be false + end + + context "when logo does not exist" do + let(:enterprise) { create(:enterprise, owner: enterprise_owner, logo: nil) } + + it "responds with error" do + spree_delete :destroy, enterprise_id: enterprise + + expect(response.status).to eq(409) + expect(json_response["error"]).to eq I18n.t("api.enterprise_logo.destroy_attachment_does_not_exist") + end + end + end + + context "as owner" do + let(:current_user) { enterprise_owner } + + it "allows removal of logo" do + spree_delete :destroy, enterprise_id: enterprise + expect(response).to be_success + end + end + + context "as super admin" do + let(:current_user) { admin_user } + + it "allows removal of logo" do + spree_delete :destroy, enterprise_id: enterprise + expect(response).to be_success + end + end + + context "as manager of other enterprise" do + let(:current_user) { other_enterprise_manager } + + it "does not allow removal of logo" do + spree_delete :destroy, enterprise_id: enterprise + expect(response.status).to eq(401) + enterprise.reload + expect(enterprise.logo?).to be true + end + end + + context "as owner of other enterprise" do + let(:current_user) { other_enterprise_owner } + + it "does not allow removal of logo" do + spree_delete :destroy, enterprise_id: enterprise + expect(response.status).to eq(401) + enterprise.reload + expect(enterprise.logo?).to be true + end + end + end + end +end diff --git a/spec/controllers/api/promo_images_controller_spec.rb b/spec/controllers/api/promo_images_controller_spec.rb new file mode 100644 index 0000000000..cce6d08f5f --- /dev/null +++ b/spec/controllers/api/promo_images_controller_spec.rb @@ -0,0 +1,90 @@ +require "spec_helper" + +module Api + describe PromoImagesController, type: :controller do + include AuthenticationWorkflow + + let(:admin_user) { create(:admin_user) } + let(:enterprise_owner) { create(:user) } + let(:enterprise) { create(:enterprise, owner: enterprise_owner ) } + let(:enterprise_manager) { create(:user, enterprise_limit: 10, enterprises: [enterprise]) } + let(:other_enterprise_owner) { create(:user) } + let(:other_enterprise) { create(:enterprise, owner: other_enterprise_owner ) } + let(:other_enterprise_manager) { create(:user, enterprise_limit: 10, enterprises: [other_enterprise]) } + + describe "removing promo image" do + image_path = File.open(Rails.root.join("app", "assets", "images", "logo-black.png")) + let(:image) { Rack::Test::UploadedFile.new(image_path, "image/png") } + + let(:enterprise) { create(:enterprise, owner: enterprise_owner, promo_image: image) } + + before do + allow(controller).to receive(:spree_current_user) { current_user } + end + + context "as manager" do + let(:current_user) { enterprise_manager } + + it "removes promo image" do + spree_delete :destroy, enterprise_id: enterprise + + expect(response).to be_success + expect(json_response["id"]).to eq enterprise.id + enterprise.reload + expect(enterprise.promo_image?).to be false + end + + context "when promo image does not exist" do + let(:enterprise) { create(:enterprise, owner: enterprise_owner, promo_image: nil) } + + it "responds with error" do + spree_delete :destroy, enterprise_id: enterprise + + expect(response.status).to eq(409) + expect(json_response["error"]).to eq I18n.t("api.enterprise_promo_image.destroy_attachment_does_not_exist") + end + end + end + + context "as owner" do + let(:current_user) { enterprise_owner } + + it "allows removal of promo image" do + spree_delete :destroy, enterprise_id: enterprise + expect(response).to be_success + end + end + + context "as super admin" do + let(:current_user) { admin_user } + + it "allows removal of promo image" do + spree_delete :destroy, enterprise_id: enterprise + expect(response).to be_success + end + end + + context "as manager of other enterprise" do + let(:current_user) { other_enterprise_manager } + + it "does not allow removal of promo image" do + spree_delete :destroy, enterprise_id: enterprise + expect(response.status).to eq(401) + enterprise.reload + expect(enterprise.promo_image?).to be true + end + end + + context "as owner of other enterprise" do + let(:current_user) { other_enterprise_owner } + + it "does not allow removal of promo image" do + spree_delete :destroy, enterprise_id: enterprise + expect(response.status).to eq(401) + enterprise.reload + expect(enterprise.promo_image?).to be true + end + end + end + end +end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 7882665618..dcd290e195 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -297,11 +297,11 @@ module Spree let!(:er_pd) { create(:enterprise_relationship, parent: d_related, child: d1, permissions_list: [:edit_profile]) } it "should be able to edit enterprises it manages" do - should have_ability([:read, :edit, :update, :bulk_update, :resend_confirmation], for: d1) + should have_ability([:read, :edit, :update, :remove_logo, :remove_promo_image, :bulk_update, :resend_confirmation], for: d1) end it "should be able to edit enterprises it has permission to" do - should have_ability([:read, :edit, :update, :bulk_update, :resend_confirmation], for: d_related) + should have_ability([:read, :edit, :update, :remove_logo, :remove_promo_image, :bulk_update, :resend_confirmation], for: d_related) end it "should be able to manage shipping methods, payment methods and enterprise fees for enterprises it manages" do From c9370672c6a38d8be7cf0ec7f3e9aeafb49d402b Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sat, 28 Jul 2018 23:44:28 +0800 Subject: [PATCH 023/190] Add JS support for removal of enterprise images --- .../resources/enterprise_resource.js.coffee | 10 +++ .../resources/services/enterprises.js.coffee | 14 ++++ .../services/enterprises_spec.js.coffee | 70 +++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee b/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee index 7cdd5b7bce..25b72e1922 100644 --- a/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee +++ b/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee @@ -8,4 +8,14 @@ angular.module("admin.resources").factory 'EnterpriseResource', ($resource) -> isArray: true 'update': method: 'PUT' + 'removeLogo': + url: '/admin/enterprises/:id/images/:action.json' + method: 'DELETE' + params: + action: 'remove_logo' + 'removePromoImage': + url: '/admin/enterprises/:id/images/:action.json' + method: 'DELETE' + params: + action: 'remove_promo_image' }) diff --git a/app/assets/javascripts/admin/resources/services/enterprises.js.coffee b/app/assets/javascripts/admin/resources/services/enterprises.js.coffee index 0b7fa6e870..435cc88500 100644 --- a/app/assets/javascripts/admin/resources/services/enterprises.js.coffee +++ b/app/assets/javascripts/admin/resources/services/enterprises.js.coffee @@ -38,3 +38,17 @@ angular.module("admin.resources").factory 'Enterprises', ($q, EnterpriseResource resetAttribute: (enterprise, attribute) -> enterprise[attribute] = @pristineByID[enterprise.id][attribute] + + performActionOnEnterpriseResource = (resourceAction) -> + (enterprise) -> + deferred = $q.defer() + resourceAction({id: enterprise.permalink}, ((data) => + @pristineByID[enterprise.id] = angular.copy(data) + deferred.resolve(data) + ), ((response) -> + deferred.reject(response) + )) + deferred.promise + + removeLogo: performActionOnEnterpriseResource(EnterpriseResource.removeLogo) + removePromoImage: performActionOnEnterpriseResource(EnterpriseResource.removePromoImage) diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee index 8c088b1715..85cf19f067 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee @@ -118,3 +118,73 @@ describe "Enterprises service", -> it "resets the specified value according to the pristine record", -> Enterprises.resetAttribute(enterprise, "name") expect(enterprise.name).toEqual "enterprise1" + + describe "#removeLogo", -> + enterprise = null + + describe "success", -> + resolved = false + + beforeEach -> + enterprise = new EnterpriseResource({ id: 15, permalink: "enterprise1", name: "Enterprise 1", logo: {} }) + $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_logo.json").respond 200, { id: 15, name: "Enterprise 1"} + Enterprises.removeLogo(enterprise).then( -> resolved = true) + $httpBackend.flush() + + it "updates the pristine copy of the enterprise", -> + expect(Enterprises.pristineByID[15]).not.toBeUndefined() + expect(Enterprises.pristineByID[15]["id"]).toEqual(15) + expect(Enterprises.pristineByID[15]["logo"]).toBeUndefined() + + it "resolves the promise", -> + expect(resolved).toBe(true) + + describe "failure", -> + rejected = false + + beforeEach -> + enterprise = new EnterpriseResource( { id: 15, permalink: "enterprise1", name: "Enterprise 1" } ) + $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_logo.json").respond 409, { error: "obj" } + Enterprises.removeLogo(enterprise).catch( -> rejected = true) + $httpBackend.flush() + + it "does not update the pristine copy of the enterprise", -> + expect(Enterprises.pristineByID[15]).toBeUndefined() + + it "rejects the promise", -> + expect(rejected).toBe(true) + + describe "#removePromoImage", -> + enterprise = null + + describe "success", -> + resolved = false + + beforeEach -> + enterprise = new EnterpriseResource({ id: 15, permalink: "enterprise1", name: "Enterprise 1", promo_image: {} }) + $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_promo_image.json").respond 200, { id: 15, name: "Enterprise 1"} + Enterprises.removePromoImage(enterprise).then( -> resolved = true) + $httpBackend.flush() + + it "updates the pristine copy of the enterprise", -> + expect(Enterprises.pristineByID[15]).not.toBeUndefined() + expect(Enterprises.pristineByID[15]["id"]).toEqual(15) + expect(Enterprises.pristineByID[15]["promo_image"]).toBeUndefined() + + it "resolves the promise", -> + expect(resolved).toBe(true) + + describe "failure", -> + rejected = false + + beforeEach -> + enterprise = new EnterpriseResource( { id: 15, permalink: "enterprise1", name: "Enterprise 1" } ) + $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_promo_image.json").respond 409, { error: "obj" } + Enterprises.removePromoImage(enterprise).catch( -> rejected = true) + $httpBackend.flush() + + it "does not update the pristine copy of the enterprise", -> + expect(Enterprises.pristineByID[15]).toBeUndefined() + + it "rejects the promise", -> + expect(rejected).toBe(true); From 368612cad6098cf177168e880d4dccab3f6090d8 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Mon, 30 Jul 2018 07:42:51 +0800 Subject: [PATCH 024/190] Allow removal of enterprise logo and promo image --- .../enterprise_controller.js.coffee | 22 +++++- .../resources/enterprise_resource.js.coffee | 8 +-- .../admin/enterprises/form/_images.html.haml | 8 ++- config/locales/en.yml | 6 ++ .../features/admin/enterprises/images_spec.rb | 30 ++++++-- spec/features/admin/enterprises_spec.rb | 12 ---- .../enterprise_controller_spec.js.coffee | 72 ++++++++++++++++++- .../services/enterprises_spec.js.coffee | 8 +-- 8 files changed, 135 insertions(+), 31 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index bb1ae6b31a..a6b0a40958 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,5 +1,5 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, $http, $window, NavigationCheck, enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu, StatusMessage) -> + .controller "enterpriseCtrl", ($scope, $http, $window, NavigationCheck, enterprise, Enterprises, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu, StatusMessage) -> $scope.Enterprise = enterprise $scope.PaymentMethods = EnterprisePaymentMethods.paymentMethods $scope.ShippingMethods = EnterpriseShippingMethods.shippingMethods @@ -67,3 +67,23 @@ angular.module("admin.enterprises") $scope.resetModal = -> $scope.newUser = $scope.invite_errors = $scope.invite_success = null + + $scope.removeLogo = -> + Enterprises.removeLogo($scope.Enterprise).then (data) -> + $scope.Enterprise = angular.copy(data) + $scope.$emit("enterprise:updated", $scope.Enterprise) + + StatusMessage.display("success", t("admin.enterprises.remove_logo.removed_successfully")) + , (response) -> + if response.data.error? + StatusMessage.display("failure", response.data.error) + + $scope.removePromoImage = -> + Enterprises.removePromoImage($scope.Enterprise).then (data) -> + $scope.Enterprise = angular.copy(data) + $scope.$emit("enterprise:updated", $scope.Enterprise) + + StatusMessage.display("success", t("admin.enterprises.remove_promo_image.removed_successfully")) + , (response) -> + if response.data.error? + StatusMessage.display("failure", response.data.error) diff --git a/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee b/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee index 25b72e1922..ec89bbda36 100644 --- a/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee +++ b/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee @@ -9,13 +9,9 @@ angular.module("admin.resources").factory 'EnterpriseResource', ($resource) -> 'update': method: 'PUT' 'removeLogo': - url: '/admin/enterprises/:id/images/:action.json' + url: '/api/enterprises/:id/logo.json' method: 'DELETE' - params: - action: 'remove_logo' 'removePromoImage': - url: '/admin/enterprises/:id/images/:action.json' + url: '/api/enterprises/:id/promo_image.json' method: 'DELETE' - params: - action: 'remove_promo_image' }) diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml index ea718607b5..d44e619cf6 100644 --- a/app/views/admin/enterprises/form/_images.html.haml +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -4,8 +4,10 @@ %br 100 x 100 pixels .omega.eight.columns - = image_tag @object.logo(:medium), class: 'image-field-group__preview-image' if @object.logo.present? + = image_tag '', class: 'image-field-group__preview-image', 'ng-src' => '{{ Enterprise.logo.medium }}', 'ng-if' => 'Enterprise.logo' = f.file_field :logo + %a.button.red{ href: '', ng: {click: 'removeLogo()', if: 'Enterprise.logo'} } + = t('admin.enterprises.remove_logo.remove') .row.page-admin-enterprises-form__promo-image-field-group.image-field-group .alpha.three.columns @@ -18,5 +20,7 @@ = t('.promo_image_note3') .omega.eight.columns - = image_tag @object.promo_image(:large), class: 'image-field-group__preview-image' if @object.promo_image.present? + = image_tag '', class: 'image-field-group__preview-image', 'ng-src' => '{{ Enterprise.promo_image.large }}', 'ng-if' => 'Enterprise.promo_image' = f.file_field :promo_image + %a.button.red{ href: '', ng: {click: 'removePromoImage()', if: 'Enterprise.promo_image'} } + = t('admin.enterprises.remove_promo_image.remove') diff --git a/config/locales/en.yml b/config/locales/en.yml index 61f74ad2a8..677b3b9d2f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -831,6 +831,12 @@ en: new: title: New Enterprise back_link: Back to enterprises list + remove_logo: + remove: "Remove Image" + removed_successfully: "Logo removed successfully" + remove_promo_image: + remove: "Remove Image" + removed_successfully: "Promo image removed successfully" welcome: welcome_title: Welcome to the Open Food Network! welcome_text: You have successfully created a diff --git a/spec/features/admin/enterprises/images_spec.rb b/spec/features/admin/enterprises/images_spec.rb index 01f8754e2d..92c4bbafaf 100644 --- a/spec/features/admin/enterprises/images_spec.rb +++ b/spec/features/admin/enterprises/images_spec.rb @@ -28,7 +28,7 @@ feature "Managing enterprise images" do scenario "editing logo" do # Adding image - attach_file "enterprise[logo]", File.join(Rails.root, "app/assets/images/logo-white.png") + attach_file "enterprise[logo]", Rails.root.join("app", "assets", "images", "logo-white.png") click_button "Update" expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") @@ -40,7 +40,7 @@ feature "Managing enterprise images" do end # Replacing image - attach_file "enterprise[logo]", File.join(Rails.root, "app/assets/images/logo-black.png") + attach_file "enterprise[logo]", Rails.root.join("app", "assets", "images", "logo-black.png") click_button "Update" expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") @@ -50,11 +50,22 @@ feature "Managing enterprise images" do expect(page).to have_selector(".image-field-group__preview-image") expect(html).to include("logo-black.png") end + + # Removing image + within ".page-admin-enterprises-form__logo-field-group" do + click_on "Remove Image" + end + + expect(page).to have_content("Logo removed successfully") + + within ".page-admin-enterprises-form__logo-field-group" do + expect(page).to have_no_selector(".image-field-group__preview-image") + end end scenario "editing promo image" do # Adding image - attach_file "enterprise[promo_image]", File.join(Rails.root, "app/assets/images/logo-white.png") + attach_file "enterprise[promo_image]", Rails.root.join("app", "assets", "images", "logo-white.png") click_button "Update" expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") @@ -66,7 +77,7 @@ feature "Managing enterprise images" do end # Replacing image - attach_file "enterprise[promo_image]", File.join(Rails.root, "app/assets/images/logo-black.png") + attach_file "enterprise[promo_image]", Rails.root.join("app", "assets", "images", "logo-black.png") click_button "Update" expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") @@ -76,6 +87,17 @@ feature "Managing enterprise images" do expect(page).to have_selector(".image-field-group__preview-image") expect(html).to include("logo-black.jpg") end + + # Removing image + within ".page-admin-enterprises-form__promo-image-field-group" do + click_on "Remove Image" + end + + expect(page).to have_content("Promo image removed successfully") + + within ".page-admin-enterprises-form__promo-image-field-group" do + expect(page).to have_no_selector(".image-field-group__preview-image") + end end end end diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index bdbb7cde16..5b1f951e95 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -417,18 +417,6 @@ feature %q{ end end - scenario "editing images for an enterprise" do - visit admin_enterprises_path - within("tbody#e_#{distributor1.id}") { click_link 'Settings' } - - within(".side_menu") do - click_link "Images" - end - - page.should have_content "LOGO" - page.should have_content "PROMO" - end - scenario "managing producer properties" do create(:property, name: "Certified Organic") visit admin_enterprises_path diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee index 17ef445846..8ea5d60837 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee @@ -4,6 +4,8 @@ describe "enterpriseCtrl", -> enterprise = null PaymentMethods = null ShippingMethods = null + Enterprises = null + StatusMessage = null beforeEach -> module('admin.enterprises') @@ -18,9 +20,11 @@ describe "enterpriseCtrl", -> shippingMethods: "shipping methods" receivesNotifications = 99 - inject ($rootScope, $controller) -> + inject ($rootScope, $controller, _Enterprises_, _StatusMessage_) -> scope = $rootScope - ctrl = $controller 'enterpriseCtrl', {$scope: scope, enterprise: enterprise, EnterprisePaymentMethods: PaymentMethods, EnterpriseShippingMethods: ShippingMethods, receivesNotifications: receivesNotifications} + Enterprises = _Enterprises_ + StatusMessage = _StatusMessage_ + ctrl = $controller "enterpriseCtrl", {$scope: scope, enterprise: enterprise, EnterprisePaymentMethods: PaymentMethods, EnterpriseShippingMethods: ShippingMethods, Enterprises: Enterprises, StatusMessage: StatusMessage, receivesNotifications: receivesNotifications} describe "initialisation", -> it "stores enterprise", -> @@ -32,6 +36,70 @@ describe "enterpriseCtrl", -> it "stores shipping methods", -> expect(scope.ShippingMethods).toBe ShippingMethods.shippingMethods + describe "removing logo", -> + deferred = null + + beforeEach inject ($q) -> + spyOn(scope, "$emit") + deferred = $q.defer() + spyOn(Enterprises, "removeLogo").and.returnValue(deferred.promise) + spyOn(StatusMessage, "display").and.callThrough() + scope.removeLogo() + + describe "when successful", -> + beforeEach inject ($rootScope) -> + deferred.resolve() + $rootScope.$digest() + + it "emits an 'enterprise:updated' event", -> + expect(scope.$emit).toHaveBeenCalledWith("enterprise:updated", scope.Enterprise) + + it "notifies user of success", -> + expect(StatusMessage.display).toHaveBeenCalledWith("success", "Logo removed successfully") + + describe "when unsuccessful", -> + beforeEach inject ($rootScope) -> + deferred.reject({ data: { error: "Logo does not exist" } }) + $rootScope.$digest() + + it "does not emit an 'enterprise:updated' event", -> + expect(scope.$emit).not.toHaveBeenCalled() + + it "notifies user of failure", -> + expect(StatusMessage.display).toHaveBeenCalledWith("failure", "Logo does not exist") + + describe "removing promo image", -> + deferred = null + + beforeEach inject ($q) -> + spyOn(scope, "$emit") + deferred = $q.defer() + spyOn(Enterprises, "removePromoImage").and.returnValue(deferred.promise) + spyOn(StatusMessage, "display").and.callThrough() + scope.removePromoImage() + + describe "when successful", -> + beforeEach inject ($rootScope) -> + deferred.resolve() + $rootScope.$digest() + + it "emits an 'enterprise:updated' event", -> + expect(scope.$emit).toHaveBeenCalledWith("enterprise:updated", scope.Enterprise) + + it "notifies user of success", -> + expect(StatusMessage.display).toHaveBeenCalledWith("success", "Promo image removed successfully") + + describe "when unsuccessful", -> + beforeEach inject ($rootScope) -> + deferred.reject({ data: { error: "Promo image does not exist" } }) + $rootScope.$digest() + + it "does not emit an 'enterprise:updated' event", -> + expect(scope.$emit).not.toHaveBeenCalled() + + it "notifies user of failure", -> + expect(StatusMessage.display).toHaveBeenCalledWith("failure", "Promo image does not exist") + describe "adding managers", -> u1 = u2 = u3 = null beforeEach -> diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee index 85cf19f067..47c669e105 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee @@ -127,7 +127,7 @@ describe "Enterprises service", -> beforeEach -> enterprise = new EnterpriseResource({ id: 15, permalink: "enterprise1", name: "Enterprise 1", logo: {} }) - $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_logo.json").respond 200, { id: 15, name: "Enterprise 1"} + $httpBackend.expectDELETE("/api/enterprises/enterprise1/logo.json").respond 200, { id: 15, name: "Enterprise 1"} Enterprises.removeLogo(enterprise).then( -> resolved = true) $httpBackend.flush() @@ -144,7 +144,7 @@ describe "Enterprises service", -> beforeEach -> enterprise = new EnterpriseResource( { id: 15, permalink: "enterprise1", name: "Enterprise 1" } ) - $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_logo.json").respond 409, { error: "obj" } + $httpBackend.expectDELETE("/api/enterprises/enterprise1/logo.json").respond 409, { error: "obj" } Enterprises.removeLogo(enterprise).catch( -> rejected = true) $httpBackend.flush() @@ -162,7 +162,7 @@ describe "Enterprises service", -> beforeEach -> enterprise = new EnterpriseResource({ id: 15, permalink: "enterprise1", name: "Enterprise 1", promo_image: {} }) - $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_promo_image.json").respond 200, { id: 15, name: "Enterprise 1"} + $httpBackend.expectDELETE("/api/enterprises/enterprise1/promo_image.json").respond 200, { id: 15, name: "Enterprise 1"} Enterprises.removePromoImage(enterprise).then( -> resolved = true) $httpBackend.flush() @@ -179,7 +179,7 @@ describe "Enterprises service", -> beforeEach -> enterprise = new EnterpriseResource( { id: 15, permalink: "enterprise1", name: "Enterprise 1" } ) - $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_promo_image.json").respond 409, { error: "obj" } + $httpBackend.expectDELETE("/api/enterprises/enterprise1/promo_image.json").respond 409, { error: "obj" } Enterprises.removePromoImage(enterprise).catch( -> rejected = true) $httpBackend.flush() From ce0758d4209a58d8880c8a38905111e41e6a70a9 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Mon, 30 Jul 2018 08:39:37 +0800 Subject: [PATCH 025/190] Add confirm dialog for enterprise image removal --- .../enterprises/controllers/enterprise_controller.js.coffee | 4 ++++ config/locales/en.yml | 2 ++ .../controllers/enterprise_controller_spec.js.coffee | 2 ++ 3 files changed, 8 insertions(+) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index a6b0a40958..4acd61e84b 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -69,6 +69,8 @@ angular.module("admin.enterprises") $scope.newUser = $scope.invite_errors = $scope.invite_success = null $scope.removeLogo = -> + return unless confirm(t("admin.enterprises.remove_logo.immediate_removal_warning")) + Enterprises.removeLogo($scope.Enterprise).then (data) -> $scope.Enterprise = angular.copy(data) $scope.$emit("enterprise:updated", $scope.Enterprise) @@ -79,6 +81,8 @@ angular.module("admin.enterprises") StatusMessage.display("failure", response.data.error) $scope.removePromoImage = -> + return unless confirm(t("admin.enterprises.remove_promo_image.immediate_removal_warning")) + Enterprises.removePromoImage($scope.Enterprise).then (data) -> $scope.Enterprise = angular.copy(data) $scope.$emit("enterprise:updated", $scope.Enterprise) diff --git a/config/locales/en.yml b/config/locales/en.yml index 677b3b9d2f..bfb9b8486d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -834,9 +834,11 @@ en: remove_logo: remove: "Remove Image" removed_successfully: "Logo removed successfully" + immediate_removal_warning: "The logo will be removed immediately after you confirm." remove_promo_image: remove: "Remove Image" removed_successfully: "Promo image removed successfully" + immediate_removal_warning: "The promo image will be removed immediately after you confirm." welcome: welcome_title: Welcome to the Open Food Network! welcome_text: You have successfully created a diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee index 8ea5d60837..475de27e29 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee @@ -42,6 +42,7 @@ describe "enterpriseCtrl", -> beforeEach inject ($q) -> spyOn(scope, "$emit") deferred = $q.defer() + spyOn(window, "confirm").and.returnValue(true) spyOn(Enterprises, "removeLogo").and.returnValue(deferred.promise) spyOn(StatusMessage, "display").and.callThrough() scope.removeLogo() @@ -74,6 +75,7 @@ describe "enterpriseCtrl", -> beforeEach inject ($q) -> spyOn(scope, "$emit") deferred = $q.defer() + spyOn(window, "confirm").and.returnValue(true) spyOn(Enterprises, "removePromoImage").and.returnValue(deferred.promise) spyOn(StatusMessage, "display").and.callThrough() scope.removePromoImage() From b23cb555253ab39ebab30792b64f5117741a2365 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Fri, 31 Aug 2018 12:10:52 +0200 Subject: [PATCH 026/190] Fix current violations of Style/Send cop --- app/controllers/admin/contents_controller.rb | 2 +- .../admin/inventory_items_controller.rb | 2 +- .../admin/product_import_controller.rb | 2 +- app/controllers/admin/resource_controller.rb | 8 +-- app/controllers/enterprises_controller.rb | 2 +- app/controllers/shop_controller.rb | 2 +- .../general_settings_controller_decorator.rb | 2 +- .../payment_methods_controller_decorator.rb | 2 +- .../admin/payments_controller_decorator.rb | 2 +- .../admin/resource_controller_decorator.rb | 2 +- .../shipping_methods_controller_decorator.rb | 2 +- app/helpers/angular_form_builder.rb | 6 +- app/helpers/angular_form_helper.rb | 2 +- app/helpers/application_helper.rb | 2 +- app/models/column_preference.rb | 4 +- app/models/model_set.rb | 2 +- app/models/product_import/entry_processor.rb | 2 +- app/models/product_import/entry_validator.rb | 2 +- .../product_import/spreadsheet_entry.rb | 2 +- app/models/proxy_order.rb | 6 +- .../spree/calculator/default_tax_decorator.rb | 6 +- .../spree/preferences/file_configuration.rb | 2 +- app/models/subscription.rb | 2 +- .../api/admin/enterprise_fee_serializer.rb | 2 +- app/services/order_syncer.rb | 4 +- lib/discourse/single_sign_on.rb | 6 +- lib/open_food_network/address_finder.rb | 22 +++--- .../enterprise_fee_calculator.rb | 71 +++++++++---------- lib/open_food_network/paperclippable.rb | 16 ++--- lib/open_food_network/scope_product_to_hub.rb | 2 +- lib/open_food_network/scope_variant_to_hub.rb | 2 +- lib/open_food_network/tag_rule_applicator.rb | 10 +-- lib/spree/api/testing_support/setup.rb | 4 +- lib/stripe/profile_storer.rb | 2 +- lib/stripe/webhook_handler.rb | 2 +- 35 files changed, 103 insertions(+), 106 deletions(-) diff --git a/app/controllers/admin/contents_controller.rb b/app/controllers/admin/contents_controller.rb index 188b74440f..9614e19d4a 100644 --- a/app/controllers/admin/contents_controller.rb +++ b/app/controllers/admin/contents_controller.rb @@ -14,7 +14,7 @@ module Admin def update params.each do |name, value| if ContentConfig.has_preference?(name) || ContentConfig.has_attachment?(name) - ContentConfig.send("#{name}=", value) + ContentConfig.public_send("#{name}=", value) end end diff --git a/app/controllers/admin/inventory_items_controller.rb b/app/controllers/admin/inventory_items_controller.rb index 432b955134..6fd382d0d5 100644 --- a/app/controllers/admin/inventory_items_controller.rb +++ b/app/controllers/admin/inventory_items_controller.rb @@ -19,7 +19,7 @@ module Admin # we can authorise #create using an object with required attributes def build_resource if parent_data.present? - parent.send(controller_name).build + parent.public_send(controller_name).build else model_class.new(params[object_name]) # This line changed end diff --git a/app/controllers/admin/product_import_controller.rb b/app/controllers/admin/product_import_controller.rb index 38ea79a97d..6f0c0c255d 100644 --- a/app/controllers/admin/product_import_controller.rb +++ b/app/controllers/admin/product_import_controller.rb @@ -53,7 +53,7 @@ module Admin @importer = ProductImport::ProductImporter.new(File.new(params[:filepath]), spree_current_user, start: params[:start], end: params[:end], settings: params[:settings]) begin - @importer.send("#{method}_entries") + @importer.public_send("#{method}_entries") rescue StandardError => e render json: e.message, response: 500 return false diff --git a/app/controllers/admin/resource_controller.rb b/app/controllers/admin/resource_controller.rb index 66151f6f75..19a98dc337 100644 --- a/app/controllers/admin/resource_controller.rb +++ b/app/controllers/admin/resource_controller.rb @@ -15,18 +15,18 @@ module Admin def edit_object_url(object, options = {}) if parent_data.present? - main_app.send "edit_admin_#{model_name}_#{object_name}_url", parent, object, options + main_app.public_send "edit_admin_#{model_name}_#{object_name}_url", parent, object, options else - main_app.send "edit_admin_#{object_name}_url", object, options + main_app.public_send "edit_admin_#{object_name}_url", object, options end end def object_url(object = nil, options = {}) target = object ? object : @object if parent_data.present? - main_app.send "admin_#{model_name}_#{object_name}_url", parent, target, options + main_app.public_send "admin_#{model_name}_#{object_name}_url", parent, target, options else - main_app.send "admin_#{object_name}_url", target, options + main_app.public_send "admin_#{object_name}_url", target, options end end diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 2b2a639b87..00af18d701 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -77,7 +77,7 @@ class EnterprisesController < BaseController def reset_user_and_customer(order) order.associate_user!(spree_current_user) if order.user.blank? || order.email.blank? - order.send(:associate_customer) if order.customer.nil? # Only associates existing customers + order.__send__(:associate_customer) if order.customer.nil? # Only associates existing customers end def reset_order_cycle(order, distributor) diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 8cac75eca2..4160bc1690 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -44,7 +44,7 @@ class ShopController < BaseController private def filtered_json(products_json) - if applicator.send(:rules).any? + if applicator.rules.any? filter(products_json) else products_json diff --git a/app/controllers/spree/admin/general_settings_controller_decorator.rb b/app/controllers/spree/admin/general_settings_controller_decorator.rb index 603f74bf63..80cdb8fa9e 100644 --- a/app/controllers/spree/admin/general_settings_controller_decorator.rb +++ b/app/controllers/spree/admin/general_settings_controller_decorator.rb @@ -10,6 +10,6 @@ module Spree @preferences_general << :bugherd_api_key end end - GeneralSettingsController.send(:prepend, GeneralSettingsEditPreferences) + GeneralSettingsController.prepend(GeneralSettingsEditPreferences) end end diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index bded58fe19..b7268276e7 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -10,7 +10,7 @@ module Spree # Only show payment methods that user has access to and sort by distributor name # ! Redundant code copied from Spree::Admin::ResourceController with modifications marked def collection - return parent.send(controller_name) if parent_data.present? + return parent.public_send(controller_name) if parent_data.present? collection = if model_class.respond_to?(:accessible_by) && !current_ability.has_block?(params[:action], model_class) diff --git a/app/controllers/spree/admin/payments_controller_decorator.rb b/app/controllers/spree/admin/payments_controller_decorator.rb index 92faeb093d..ccbdef25d5 100644 --- a/app/controllers/spree/admin/payments_controller_decorator.rb +++ b/app/controllers/spree/admin/payments_controller_decorator.rb @@ -10,7 +10,7 @@ Spree::Admin::PaymentsController.class_eval do # Because we have a transition method also called void, we do this to avoid conflicts. event = "void_transaction" if event == "void" - if @payment.send("#{event}!") + if @payment.public_send("#{event}!") flash[:success] = t(:payment_updated) else flash[:error] = t(:cannot_perform_operation) diff --git a/app/controllers/spree/admin/resource_controller_decorator.rb b/app/controllers/spree/admin/resource_controller_decorator.rb index cb789d7330..19293545ec 100644 --- a/app/controllers/spree/admin/resource_controller_decorator.rb +++ b/app/controllers/spree/admin/resource_controller_decorator.rb @@ -13,4 +13,4 @@ module AuthorizeOnLoadResource end end -Spree::Admin::ResourceController.send(:prepend, AuthorizeOnLoadResource) +Spree::Admin::ResourceController.prepend(AuthorizeOnLoadResource) diff --git a/app/controllers/spree/admin/shipping_methods_controller_decorator.rb b/app/controllers/spree/admin/shipping_methods_controller_decorator.rb index 9b2b03cf5e..72964fe325 100644 --- a/app/controllers/spree/admin/shipping_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/shipping_methods_controller_decorator.rb @@ -7,7 +7,7 @@ module Spree # Sort shipping methods by distributor name # ! Code copied from Spree::Admin::ResourceController with two added lines def collection - return parent.send(controller_name) if parent_data.present? + return parent.public_send(controller_name) if parent_data.present? collection = if model_class.respond_to?(:accessible_by) && !current_ability.has_block?(params[:action], model_class) diff --git a/app/helpers/angular_form_builder.rb b/app/helpers/angular_form_builder.rb index 1f1ed29550..7d87395bc6 100644 --- a/app/helpers/angular_form_builder.rb +++ b/app/helpers/angular_form_builder.rb @@ -7,10 +7,6 @@ class AngularFormBuilder < ActionView::Helpers::FormBuilder end def ng_text_field(method, options = {}) - # @object_name --> "enterprise_fee_set" - # @fields_for_record_name --> :collection - # @object.send(@fields_for_record_name).first.class.to_s.underscore --> enterprise_fee - value = "{{ #{angular_model(method)} }}" options.reverse_merge!({'id' => angular_id(method)}) @@ -46,6 +42,6 @@ class AngularFormBuilder < ActionView::Helpers::FormBuilder end def angular_model(method) - "#{@object.send(@fields_for_record_name).first.class.to_s.underscore}.#{method}" + "#{@object.public_send(@fields_for_record_name).first.class.to_s.underscore}.#{method}" end end diff --git a/app/helpers/angular_form_helper.rb b/app/helpers/angular_form_helper.rb index 0687f62188..2528e8bc7e 100644 --- a/app/helpers/angular_form_helper.rb +++ b/app/helpers/angular_form_helper.rb @@ -11,7 +11,7 @@ module AngularFormHelper def ng_options_from_collection_for_select(collection, value_method, text_method, angular_field) options = collection.map do |element| - [element.send(text_method), element.send(value_method)] + [element.public_send(text_method), element.public_send(value_method)] end ng_options_for_select(options, angular_field) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bec9d181c9..f61a11727c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -15,7 +15,7 @@ module ApplicationHelper # spree.foo_path in any view rendered from non-spree-namespaced controllers. def method_missing(method, *args, &block) if (method.to_s.end_with?('_path') || method.to_s.end_with?('_url')) && spree.respond_to?(method) - spree.send(method, *args) + spree.public_send(method, *args) else super end diff --git a/app/models/column_preference.rb b/app/models/column_preference.rb index 8def9bde30..f6bba869f7 100644 --- a/app/models/column_preference.rb +++ b/app/models/column_preference.rb @@ -20,7 +20,7 @@ class ColumnPreference < ActiveRecord::Base def self.for(user, action_name) stored_preferences = where(user_id: user.id, action_name: action_name) - default_preferences = send("#{action_name}_columns") + default_preferences = __send__("#{action_name}_columns") filter(default_preferences, user, action_name) default_preferences.each_with_object([]) do |(column_name, default_attributes), preferences| stored_preference = stored_preferences.find_by_column_name(column_name) @@ -37,7 +37,7 @@ class ColumnPreference < ActiveRecord::Base private def self.valid_columns_for(action_name) - send("#{action_name}_columns").keys.map(&:to_s) + __send__("#{action_name}_columns").keys.map(&:to_s) end def self.known_actions diff --git a/app/models/model_set.rb b/app/models/model_set.rb index cb75f929ab..b73c15f7df 100644 --- a/app/models/model_set.rb +++ b/app/models/model_set.rb @@ -12,7 +12,7 @@ class ModelSet @collection = attributes[:collection] if attributes[:collection] attributes.each do |name, value| - send("#{name}=", value) + public_send("#{name}=", value) end end diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 97f9cbe55c..2271507bbf 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -204,7 +204,7 @@ module ProductImport when 'overwrite_all' object.assign_attributes(attribute => setting['value']) when 'overwrite_empty' - if object.send(attribute).blank? || ((attribute == 'on_hand' || attribute == 'count_on_hand') && entry.on_hand_nil) + if object.public_send(attribute).blank? || ((attribute == 'on_hand' || attribute == 'count_on_hand') && entry.on_hand_nil) object.assign_attributes(attribute => setting['value']) end end diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 4f48c7745c..a73fa4a78d 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -169,7 +169,7 @@ module ProductImport return if category.blank? if index.key? category - entry.send("#{type}_category_id=", index[category]) + entry.public_send("#{type}_category_id=", index[category]) else mark_as_invalid(entry, attribute: "#{type}_category", error: I18n.t('admin.product_import.model.not_found')) end diff --git a/app/models/product_import/spreadsheet_entry.rb b/app/models/product_import/spreadsheet_entry.rb index 8b3e2d54f0..fed45a6645 100644 --- a/app/models/product_import/spreadsheet_entry.rb +++ b/app/models/product_import/spreadsheet_entry.rb @@ -71,7 +71,7 @@ module ProductImport units.converted_attributes.each do |attr, value| if respond_to?("#{attr}=") - send("#{attr}=", value) unless non_product_attributes.include?(attr) + public_send("#{attr}=", value) unless non_product_attributes.include?(attr) end end end diff --git a/app/models/proxy_order.rb b/app/models/proxy_order.rb index 8c983faa91..bcfa1b1cef 100644 --- a/app/models/proxy_order.rb +++ b/app/models/proxy_order.rb @@ -19,7 +19,7 @@ class ProxyOrder < ActiveRecord::Base def state # NOTE: the order is important here %w(canceled paused pending cart).each do |state| - return state if send("#{state}?") + return state if __send__("#{state}?") end order.state end @@ -32,7 +32,7 @@ class ProxyOrder < ActiveRecord::Base return false unless order_cycle.orders_close_at.andand > Time.zone.now transaction do update_column(:canceled_at, Time.zone.now) - order.send('cancel') if order + order.cancel if order true end end @@ -41,7 +41,7 @@ class ProxyOrder < ActiveRecord::Base return false unless order_cycle.orders_close_at.andand > Time.zone.now transaction do update_column(:canceled_at, nil) - order.send('resume') if order + order.resume if order true end end diff --git a/app/models/spree/calculator/default_tax_decorator.rb b/app/models/spree/calculator/default_tax_decorator.rb index 45149b6d4d..a70bb4f8ba 100644 --- a/app/models/spree/calculator/default_tax_decorator.rb +++ b/app/models/spree/calculator/default_tax_decorator.rb @@ -18,15 +18,15 @@ Spree::Calculator::DefaultTax.class_eval do # Added this block, finds relevant fees for each line_item, calculates the tax on them, and returns the total tax per_item_fees_total = order.line_items.sum do |line_item| - calculator.send(:per_item_enterprise_fee_applicators_for, line_item.variant) + calculator.per_item_enterprise_fee_applicators_for(line_item.variant) .select { |applicator| (!applicator.enterprise_fee.inherits_tax_category && applicator.enterprise_fee.tax_category == rate.tax_category) || - (applicator.enterprise_fee.inherits_tax_category && line_item.product.tax_category == rate.tax_category) + (applicator.enterprise_fee.inherits_tax_category && line_item.product.tax_category == rate.tax_category) } .sum { |applicator| applicator.enterprise_fee.compute_amount(line_item) } end # Added this block, finds relevant fees for whole order, calculates the tax on them, and returns the total tax - per_order_fees_total = calculator.send(:per_order_enterprise_fee_applicators_for, order) + per_order_fees_total = calculator.per_order_enterprise_fee_applicators_for(order) .select { |applicator| applicator.enterprise_fee.tax_category == rate.tax_category } .sum { |applicator| applicator.enterprise_fee.compute_amount(order) } diff --git a/app/models/spree/preferences/file_configuration.rb b/app/models/spree/preferences/file_configuration.rb index 02c5ae3646..f918f52af5 100644 --- a/app/models/spree/preferences/file_configuration.rb +++ b/app/models/spree/preferences/file_configuration.rb @@ -15,7 +15,7 @@ module Spree::Preferences def get_preference(key) if !has_preference?(key) && has_attachment?(key) - send key + public_send key else super key end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 165b063ae0..f217bcb0ec 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -51,7 +51,7 @@ class Subscription < ActiveRecord::Base def state # NOTE: the order is important here %w(canceled paused pending ended).each do |state| - return state if send("#{state}?") + return state if __send__("#{state}?") end "active" end diff --git a/app/serializers/api/admin/enterprise_fee_serializer.rb b/app/serializers/api/admin/enterprise_fee_serializer.rb index 96c279d1c0..7bf2ea4e14 100644 --- a/app/serializers/api/admin/enterprise_fee_serializer.rb +++ b/app/serializers/api/admin/enterprise_fee_serializer.rb @@ -15,7 +15,7 @@ class Api::Admin::EnterpriseFeeSerializer < ActiveModel::Serializer result = nil - options[:controller].send(:with_format, :html) do + options[:controller].__send__(:with_format, :html) do result = options[:controller].render_to_string :partial => 'admin/enterprise_fees/calculator_settings', :locals => {:enterprise_fee => object} end diff --git a/app/services/order_syncer.rb b/app/services/order_syncer.rb index c7b7c87195..de7aa44790 100644 --- a/app/services/order_syncer.rb +++ b/app/services/order_syncer.rb @@ -83,7 +83,7 @@ class OrderSyncer def addresses_match?(order_address, subscription_address) relevant_address_attrs.all? do |attr| - order_address[attr] == subscription_address.send("#{attr}_was") || + order_address[attr] == subscription_address.public_send("#{attr}_was") || order_address[attr] == subscription_address[attr] end end @@ -101,7 +101,7 @@ class OrderSyncer # address on the order matches the shop's address def force_ship_address_required?(order) return false unless shipping_method.require_ship_address? - distributor_address = order.send(:address_from_distributor) + distributor_address = order.__send__(:address_from_distributor) relevant_address_attrs.all? do |attr| order.ship_address[attr] == distributor_address[attr] end diff --git a/lib/discourse/single_sign_on.rb b/lib/discourse/single_sign_on.rb index edc1acfa79..2e39f292a8 100644 --- a/lib/discourse/single_sign_on.rb +++ b/lib/discourse/single_sign_on.rb @@ -42,7 +42,7 @@ module Discourse if BOOLS.include? k val = ["true", "false"].include?(val) ? val == "true" : nil end - sso.send("#{k}=", val) + sso.public_send("#{k}=", val) end decoded_hash.each do |k,v| @@ -87,9 +87,9 @@ module Discourse def unsigned_payload payload = {} ACCESSORS.each do |k| - next if (val = send k) == nil + next if (val = public_send k) == nil - payload[k] = val + payload[k] = val end if @custom_fields diff --git a/lib/open_food_network/address_finder.rb b/lib/open_food_network/address_finder.rb index 6c19f153bd..02f2aa911a 100644 --- a/lib/open_food_network/address_finder.rb +++ b/lib/open_food_network/address_finder.rb @@ -12,7 +12,7 @@ module OpenFoodNetwork args.each do |arg| type = types[arg.class] next unless type - send("#{type}=", arg) + public_send("#{type}=", arg) end end @@ -24,16 +24,6 @@ module OpenFoodNetwork customer_preferred_ship_address || user_preferred_ship_address || fallback_ship_address end - private - - def types - { - String => "email", - Customer => "customer", - Spree::User => "user" - } - end - def email=(arg) @email ||= arg end @@ -46,6 +36,16 @@ module OpenFoodNetwork @user ||= arg end + private + + def types + { + String => "email", + Customer => "customer", + Spree::User => "user" + } + end + def customer_preferred_bill_address customer.andand.bill_address end diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb index ee3fa7648c..b6816cc44b 100644 --- a/lib/open_food_network/enterprise_fee_calculator.rb +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -58,6 +58,41 @@ module OpenFoodNetwork end end + def per_item_enterprise_fee_applicators_for(variant) + fees = [] + + return [] unless @order_cycle && @distributor + + @order_cycle.exchanges_carrying(variant, @distributor).each do |exchange| + exchange.enterprise_fees.per_item.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) + end + end + + @order_cycle.coordinator_fees.per_item.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator') + end + + fees + end + + def per_order_enterprise_fee_applicators_for(order) + fees = [] + + return fees unless @order_cycle && order.distributor + + @order_cycle.exchanges_supplying(order).each do |exchange| + exchange.enterprise_fees.per_order.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) + end + end + + @order_cycle.coordinator_fees.per_order.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator') + end + + fees + end private @@ -104,41 +139,5 @@ module OpenFoodNetwork line_item = OpenStruct.new variant: variant, quantity: 1, price: variant.price, amount: variant.price enterprise_fee.compute_amount(line_item) end - - def per_item_enterprise_fee_applicators_for(variant) - fees = [] - - return [] unless @order_cycle && @distributor - - @order_cycle.exchanges_carrying(variant, @distributor).each do |exchange| - exchange.enterprise_fees.per_item.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) - end - end - - @order_cycle.coordinator_fees.per_item.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator') - end - - fees - end - - def per_order_enterprise_fee_applicators_for(order) - fees = [] - - return fees unless @order_cycle && order.distributor - - @order_cycle.exchanges_supplying(order).each do |exchange| - exchange.enterprise_fees.per_order.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) - end - end - - @order_cycle.coordinator_fees.per_order.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator') - end - - fees - end end end diff --git a/lib/open_food_network/paperclippable.rb b/lib/open_food_network/paperclippable.rb index 82451651c8..580f8507b2 100644 --- a/lib/open_food_network/paperclippable.rb +++ b/lib/open_food_network/paperclippable.rb @@ -4,18 +4,18 @@ module OpenFoodNetwork module Paperclippable def self.included(base) - base.send :extend, ActiveModel::Naming - base.send :extend, ActiveModel::Callbacks - base.send :include, ActiveModel::Validations - base.send :include, Paperclip::Glue + base.extend(ActiveModel::Naming) + base.extend(ActiveModel::Callbacks) + base.include(ActiveModel::Validations) + base.include(Paperclip::Glue) # Paperclip required callbacks - base.send :define_model_callbacks, :save, only: [:after] - base.send :define_model_callbacks, :commit, only: [:after] - base.send :define_model_callbacks, :destroy, only: [:before, :after] + base.define_model_callbacks(:save, only: [:after]) + base.define_model_callbacks(:commit, only: [:after]) + base.define_model_callbacks(:destroy, only: [:before, :after]) # Initialise an ID - base.send :attr_accessor, :id + base.__send__(:attr_accessor, :id) base.instance_variable_set :@id, 1 end diff --git a/lib/open_food_network/scope_product_to_hub.rb b/lib/open_food_network/scope_product_to_hub.rb index 98ffbd84ff..f06eea430b 100644 --- a/lib/open_food_network/scope_product_to_hub.rb +++ b/lib/open_food_network/scope_product_to_hub.rb @@ -8,7 +8,7 @@ module OpenFoodNetwork end def scope(product) - product.send :extend, OpenFoodNetwork::ScopeProductToHub::ScopeProductToHub + product.extend(OpenFoodNetwork::ScopeProductToHub::ScopeProductToHub) product.instance_variable_set :@hub, @hub product.instance_variable_set :@variant_overrides, @variant_overrides end diff --git a/lib/open_food_network/scope_variant_to_hub.rb b/lib/open_food_network/scope_variant_to_hub.rb index feb99ae7c3..bc2afa63da 100644 --- a/lib/open_food_network/scope_variant_to_hub.rb +++ b/lib/open_food_network/scope_variant_to_hub.rb @@ -6,7 +6,7 @@ module OpenFoodNetwork end def scope(variant) - variant.send :extend, OpenFoodNetwork::ScopeVariantToHub::ScopeVariantToHub + variant.extend(OpenFoodNetwork::ScopeVariantToHub::ScopeVariantToHub) variant.instance_variable_set :@hub, @hub variant.instance_variable_set :@variant_override, @variant_overrides[variant] end diff --git a/lib/open_food_network/tag_rule_applicator.rb b/lib/open_food_network/tag_rule_applicator.rb index 5225dfdd86..803b426f69 100644 --- a/lib/open_food_network/tag_rule_applicator.rb +++ b/lib/open_food_network/tag_rule_applicator.rb @@ -24,6 +24,11 @@ module OpenFoodNetwork end end + def rules + return @rules unless @rules.nil? + @rules = rule_class.prioritised.for(enterprise) + end + private def reject?(element) @@ -38,11 +43,6 @@ module OpenFoodNetwork false end - def rules - return @rules unless @rules.nil? - @rules = rule_class.prioritised.for(enterprise) - end - def customer_rules return @customer_matched_rules unless @customer_matched_rules.nil? @customer_matched_rules = rules.select{ |rule| customer_tags_match?(rule) } diff --git a/lib/spree/api/testing_support/setup.rb b/lib/spree/api/testing_support/setup.rb index b8ffcd50f9..db8633e252 100644 --- a/lib/spree/api/testing_support/setup.rb +++ b/lib/spree/api/testing_support/setup.rb @@ -20,7 +20,9 @@ module Spree let!(:current_api_user) do user = create(:user) user.spree_roles = [] - enterprises.each { |e| user.enterprise_roles.create(enterprise: send(e)) } + enterprises.each do |enterprise| + user.enterprise_roles.create(enterprise: public_send(enterprise)) + end user.save! user end diff --git a/lib/stripe/profile_storer.rb b/lib/stripe/profile_storer.rb index 9b1579228d..1b8928e884 100644 --- a/lib/stripe/profile_storer.rb +++ b/lib/stripe/profile_storer.rb @@ -17,7 +17,7 @@ module Stripe attrs = source_attrs_from(response) @payment.source.update_attributes!(attrs) else - @payment.send(:gateway_error, response.message) + @payment.__send__(:gateway_error, response.message) end end diff --git a/lib/stripe/webhook_handler.rb b/lib/stripe/webhook_handler.rb index 875dd9e52b..231eb78b03 100644 --- a/lib/stripe/webhook_handler.rb +++ b/lib/stripe/webhook_handler.rb @@ -6,7 +6,7 @@ module Stripe def handle return :unknown unless known_event? - send(event_mappings[@event.type]) + __send__(event_mappings[@event.type]) end private From 138fa41b8d119bf2db5e8fb0cf2890ab16c96cac Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Tue, 4 Sep 2018 20:03:55 +0100 Subject: [PATCH 027/190] Fetched latest translations from transifex with tx pull --force --- config/locales/de_DE.yml | 491 ++++++++++++++++++++------------------- config/locales/en_GB.yml | 284 +++++++++++++++------- config/locales/en_US.yml | 178 +++++++------- config/locales/es.yml | 244 ++++++++++++------- config/locales/fr.yml | 50 +++- config/locales/fr_CA.yml | 28 ++- config/locales/it.yml | 118 +++++++--- config/locales/nb.yml | 137 ++++++++--- config/locales/pt.yml | 176 +++++++------- config/locales/sv.yml | 142 +++++++---- 10 files changed, 1138 insertions(+), 710 deletions(-) diff --git a/config/locales/de_DE.yml b/config/locales/de_DE.yml index 4f7616a939..938ee71f31 100644 --- a/config/locales/de_DE.yml +++ b/config/locales/de_DE.yml @@ -19,7 +19,7 @@ de_DE: email: taken: "Es gibt bereits ein Konto für diese E-Mail-Adresse. Bitte versuchen Sie sich einzuloggen oder setzen Sie Ihr Passwort zurück." spree/order: - no_card: Es gibt keine autorisierten Kreditkarten, auf die zugegriffen werden kann. + no_card: Es sind keine belastbaren Karten verfügbar. order_cycle: attributes: orders_close_at: @@ -40,8 +40,8 @@ de_DE: not_coordinated_by_shop: "wird nicht von %{shop} koordiniert" payment_method: not_available_to_shop: "ist nicht verfügbar für %{shop}" - invalid_type: "muss eine Bar- oder Stripe-Methode sein" - charges_not_allowed: "Kreditkartebehebungen von diesem Kunden sind nicht gestattet." + invalid_type: "muss Bar oder Stripe sein" + charges_not_allowed: "Der Kunde hat keine Belastungen erlaubt. " no_default_card: "^ Für diesen Kunden ist keine Standardkarte verfügbar" shipping_method: not_available_to_shop: "ist nicht verfügbar für %{shop}" @@ -219,7 +219,7 @@ de_DE: quantity: Menge schedule: Plan shipping: Versand - shipping_method: Versandart + shipping_method: Lieferart shop: Laden sku: Artikelnummer status_state: Status @@ -256,11 +256,11 @@ de_DE: unsaved_changes: "Sie haben ungespeicherte Änderungen" accounts_and_billing_settings: method_settings: - default_accounts_payment_method: "Zahlungsmethode für Standardkonten" - default_accounts_shipping_method: "Standardmethode für den Kontoversand" + default_accounts_payment_method: "Zahlungsart für Standardkonten" + default_accounts_shipping_method: "Lieferart für Standardkonten" edit: accounts_and_billing: "Konten und Abrechung" - accounts_administration_distributor: "Kontenadministrator" + accounts_administration_distributor: "Kontenverwaltung-Verteiler" admin_settings: "Einstellungen" update_invoice: "Rechnungen aktualisieren" auto_update_invoices: "Rechnungen automatisch jede Nacht um 1:00 Uhr aktualisieren" @@ -325,11 +325,11 @@ de_DE: enable_receipt_printing?: Optionen zum Drucken von Belegen mit Thermodruckern in der Dropdown-Liste anzeigen? stripe_connect_settings: edit: - title: "Streifen verbinden" - settings: "die Einstellungen" + title: "Stripe Connect" + settings: "Einstellungen" stripe_connect_enabled: Läden erlauben, Zahlungen über Stripe Connect anzunehmen? no_api_key_msg: Für dieses Unternehmen existiert kein Stripe-Konto. - configuration_explanation_html: Detaillierte Anweisungen zur Konfiguration der Stripe Connect-Integration finden Sie unter konsultieren Sie diese Anleitung . + configuration_explanation_html: Detaillierte Anweisungen zur Konfiguration der Stripe Connect-Integration finden Sie unter der Anleitung . status: Status ok: OK instance_secret_key: Instanzgeheimschlüssel @@ -338,7 +338,7 @@ de_DE: charges_enabled: Gebühren aktiviert charges_enabled_warning: "Warnung: Gebühren sind für Ihr Konto nicht aktiviert" auth_fail_error: Der von Ihnen angegebene API-Schlüssel ist ungültig - empty_api_key_error_html: Es wurde kein Stripe-API-Schlüssel bereitgestellt. Um den API-Schlüssel festzulegen, folgen Sie bitte diesen Anweisungen < / a> + empty_api_key_error_html: Es wurde kein Stripe-API-Schlüssel bereitgestellt. Um den API-Schlüssel festzulegen, folgen Sie bitte diesen Anweisungen matomo_settings: edit: title: "Matomo-Einstellungen" @@ -443,7 +443,7 @@ de_DE: index: select_file: Wählen Sie eine Tabelle zum Hochladen spreadsheet: Kalkulationstabelle - choose_import_type: Wählen Sie den Importtyp aus + choose_import_type: Wählen Sie die Importart import_into: Importart product_list: Produktliste inventories: Bestände @@ -473,7 +473,7 @@ de_DE: no_permission: Sie sind nicht berechtigt, dieses Unternehmen zu verwalten not_found: Unternehmen konnte nicht in der Datenbank gefunden werden no_name: Kein Name - blank_supplier: Manche Produkte haben einen leeren Lieferantennamen + blank_supplier: Manche Produkte haben einen leeren Anbieternamen reset_absent?: Fehlende Produkte zurücksetzen reset_absent_tip: Den Bestand für alle nicht in der Datei vorhandenen Produkte auf Null setzen overwrite_all: Alles überschreiben @@ -503,7 +503,7 @@ de_DE: inventory_reset: Bei Katalogeinträgen wurde der Bestand auf null zurückgesetzt all_saved: "Alle Artikel wurden erfolgreich gespeichert" some_saved: "Artikel wurden erfolgreich gespeichert" - save_errors: Fehler speichern + save_errors: Fehler beim Speichern import_again: Eine andere Datei hochladen view_products: Zur Produktseite gehen view_inventory: Zur Katalogseite gehen @@ -576,10 +576,10 @@ de_DE: desc_long: Über uns desc_long_placeholder: Schreiben Sie etwas über sich. Diese Information wird in Ihrem öffentlichen Profil angezeigt. business_details: - abn: ABN - abn_placeholder: z.B. 99 123 456 789 - acn: ACN - acn_placeholder: z.B. 123 456 789 + abn: USt-IdNr. + abn_placeholder: z.B. DE999999999 + acn: St.-Nr. + acn_placeholder: z.B. 93815/08152 display_invoice_logo: Logo in Rechnungen anzeigen invoice_text: Fügen Sie benutzerdefinierten Text am Ende der Rechnungen hinzu contact: @@ -597,45 +597,45 @@ de_DE: fee_type: Art der Gebühr manage_fees: Unternehmensgebühren verwalten no_fees_yet: Sie haben noch keine Unternehmensgebühren. - create_button: Erstelle jetzt einen + create_button: Erstelle jetzt eine images: logo: Logo promo_image_placeholder: 'Dieses Bild wird in "Über uns" angezeigt' promo_image_note1: 'BITTE BEACHTEN SIE:' - promo_image_note2: Jedes hier hochgeladene Promo-Bild wird auf 1200 x 260 beschnitten. + promo_image_note2: Jedes hier hochgeladene Werbebild wird auf 1200 x 260 beschnitten. promo_image_note3: Das Werbebild wird oben auf der Profilseite eines Unternehmens und in Pop-ups angezeigt. inventory_settings: - text1: Sie können sich dafür entscheiden, Lagerbestände und Preise über Ihre zu verwalten + text1: Sie verwalten optional Ihre Lagerbestände und Preise auch in Ihrem inventory: Katalog text2: > Wenn Sie das Katalog-Tool verwenden, können Sie auswählen, ob neue Produkte, - die von Ihren Lieferanten hinzugefügt wurden, zu Ihrem Katalog hinzugefügt + die von Ihren Anbieter hinzugefügt wurden, zu Ihrem Katalog hinzugefügt werden müssen, bevor sie verkauft werden können. Wenn Sie Ihren Katalog nicht zur Verwaltung Ihrer Produkte verwenden, sollten Sie die folgende - Option auswählen: + "empfohlene" Option auswählen: preferred_product_selection_from_inventory_only_yes: Neue Produkte können in meinem Laden angeboten werden (empfohlen) preferred_product_selection_from_inventory_only_no: Neue Produkte müssen zu meinem Katalog hinzugefügt werden, bevor sie in meinem Laden erscheinen können payment_methods: name: Name applies: Gilt? - manage: Zahlungsmethoden verwalten - not_method_yet: noch keine Zahlungsmethoden - create_button: Neue Zahlungsmethode - create_one_button: Erstelle jetzt einen + manage: Zahlungsarten verwalten + not_method_yet: Sie haben noch keine Zahlungsarten + create_button: Neue Zahlungsart + create_one_button: Erstelle jetzt eine primary_details: name: Name name_placeholder: z.B. Professor Plums biodynamische Trüffel groups: Gruppen groups_tip: Wählen Sie beliebige Gruppen oder Regionen aus, bei denen Sie Mitglied sind. Dies hilft Kunden, Ihr Unternehmen zu finden. groups_placeholder: Beginnen Sie mit der Eingabe, um nach verfügbaren Gruppen zu suchen ... - primary_producer: primärer Erzeuger - primary_producer_tip: Wählen Sie "Produzent", wenn Sie ein Hauptproduzent von Lebensmitteln sind. - producer: Produzent - any: Egal + primary_producer: Erzeuger? + primary_producer_tip: Wählen Sie "Erzeuger", wenn Sie ein Erzeuger von Lebensmitteln sind. + producer: Erzeuger + any: Alle none: Keine own: Eigene sells: vertreibt - sells_tip: "None - Unternehmen verkauft nicht direkt an Kunden.
Own - Enterprise verkauft eigene Produkte an Kunden.
Any - Enterprise kann eigene oder andere Unternehmensprodukte verkaufen.
" + sells_tip: "Keine - Unternehmen verkauft nicht direkt an Kunden.
Eigene - Unternehmen verkauft eigene Produkte an Kunden.
Alle - Unternehmen verkauft eigene Produkte und Produkte anderer.
" visible_in_search: öffentlich sichtbar in der Suche visible_in_search_tip: Bestimmt, ob dieses Unternehmen für Kunden beim Durchsuchen der Website sichtbar ist. visible: öffentlich sichtbar @@ -647,27 +647,27 @@ de_DE: shipping_methods: name: Name applies: Gilt? - manage: Versandmethoden verwalten - create_button: Erstellen Sie eine neue Versandmethode - create_one_button: Erstelle jetzt einen - no_method_yet: Sie haben noch keine Versandmethoden. + manage: Lieferarten verwalten + create_button: Neue Lieferart erstellen + create_one_button: Erstelle jetzt eine + no_method_yet: Sie haben noch keine Lieferarten. shop_preferences: shopfront_requires_login: "Öffentlich sichtbarer Laden?" shopfront_requires_login_tip: "Wählen Sie aus, ob sich Kunden anmelden müssen, um den Laden zu sehen oder ob sie für alle sichtbar sind." - shopfront_requires_login_false: "Öffentlich sichtbar" + shopfront_requires_login_false: "Öffentlich" shopfront_requires_login_true: "Nur für registrierte Nutzer sichtbar" - recommend_require_login: "Wenn Bestellungen nachträglich geändert werden dürfen, dann emfehlen wir Einkauf nur für eingeloggte Nutzer." + recommend_require_login: "Wenn Bestellungen nachträglich geändert werden dürfen, empfehlen wir Einkauf nur für eingeloggte Nutzer." allow_guest_orders: "Gast Einkauf" allow_guest_orders_tip: "Gasteinkauf erlauben oder nur für eingeloggte Nutzer" allow_guest_orders_false: "Einkauf nur für eingeloggte Nutzer" - allow_guest_orders_true: "Gast Einkauf erlauben" + allow_guest_orders_true: "Gasteinkauf erlauben" allow_order_changes: "Bestellungen nachträglich ändern" allow_order_changes_tip: "Kunden erlauben, ihre Bestellung zu ändern, solange der Bestellzyklus offen ist." - allow_order_changes_false: "Platzierte Bestellungen können nicht geändert / storniert werden" + allow_order_changes_false: "Bestellungen können nicht geändert / storniert werden" allow_order_changes_true: "Kunden können Bestellungen ändern oder stornieren, während der Bestellzyklus geöffnet ist" enable_subscriptions: "Abonnements" enable_subscriptions_tip: "Abo-Funktionalität aktivieren?" - enable_subscriptions_false: "Behindert" + enable_subscriptions_false: "deaktiviert" enable_subscriptions_true: "aktiviert" shopfront_message: Laden-Nachricht shopfront_message_placeholder: > @@ -686,16 +686,16 @@ de_DE: social: twitter_placeholder: z.B. @the_prof stripe_connect: - connect_with_stripe: "Verbinde dich mit Streifen" + connect_with_stripe: "Stripe integrieren" stripe_connect_intro: "Um Zahlungen mit Kreditkarte zu akzeptieren, müssen Sie Ihr Stripe-Konto mit dem Open Food Network verbinden. Verwenden Sie den Knopf rechts, um loszulegen." stripe_account_connected: "Stripe-Konto verbunden." disconnect: "Trennen Sie das Konto" confirm_modal: - title: Verbinde dich mit Streifen + title: Stripe integrieren part1: Stripe ist ein Zahlungsverarbeitungsdienst, der es Geschäften im OFN ermöglicht, Kreditkartenzahlungen von Kunden zu akzeptieren. - part2: Um diese Funktion zu verwenden, müssen Sie Ihr Stripe-Konto mit dem OFN verbinden. Wenn Sie unten auf "Ich stimme zu" klicken, wird die Stripe-Website an Sie weitergeleitet, wo Sie ein bestehendes Stripe-Konto verbinden oder ein neues erstellen können. + part2: Um diese Funktion zu verwenden, müssen Sie Ihr Stripe-Konto mit dem OFN verbinden. Klicken Sie "Zustimmen", um auf die Stripe-Website weitergeleitet zu werden, wo Sie ein bestehendes Stripe-Konto verbinden oder ein neues erstellen können. part3: Dadurch kann das Open Food Network Kreditkartenzahlungen von Kunden in Ihrem Namen akzeptieren. Bitte beachten Sie, dass Sie ein eigenes Stripe-Konto unterhalten müssen, die Gebühren für Stripe-Gebühren bezahlen und etwaige Rückbuchungen und Kundenservice selbst vornehmen müssen. - i_agree: Ich stimme zu + i_agree: Zustimmen cancel: Stornieren tag_rules: default_rules: @@ -705,22 +705,22 @@ de_DE: no_tags_yet: Für dieses Unternehmen sind noch keine Tags vorhanden no_rules_yet: Für dieses Tag gelten noch keine Regeln for_customers_tagged: 'Für Kunden mit dem Tag:' - add_new_rule: '+ Fügen Sie eine neue Regel hinzu' - add_new_tag: '+ Hinzufügen eines neuen Tags' + add_new_rule: '+ Neue Regel hinzufügen' + add_new_tag: '+ Neuen Tag hinzufügen' users: email_confirmation_notice_html: "E-Mail-Bestätigung steht aus. Wir haben eine Bestätigungs-E-Mail an %{email} gesendet." resend: Erneut senden owner: 'Inhaber' contact: "Kontakt" contact_tip: "Der Manager, der Enterprise-E-Mails für Bestellungen und Benachrichtigungen erhält. Muss eine bestätigte E-Mail-Adresse haben." - owner_tip: Der primäre Benutzer, der für dieses Unternehmen verantwortlich ist. + owner_tip: Der Hauptnutzer, der für dieses Unternehmen verantwortlich ist. notifications: Benachrichtigungen notifications_tip: Benachrichtigungen über Bestellungen werden an diese E-Mail-Adresse gesendet. notifications_placeholder: z.B. lisa@maier.de notifications_note: 'Hinweis: Eine neue E-Mail-Adresse muss möglicherweise vor der Verwendung bestätigt werden' managers: Manager - managers_tip: Die anderen Benutzer mit der Berechtigung, dieses Unternehmen zu verwalten. - invite_manager: "Einladungsmanager" + managers_tip: Andere Benutzer mit der Berechtigung, dieses Unternehmen zu verwalten. + invite_manager: "Manager einladen" invite_manager_tip: "Laden Sie einen nicht registrierten Benutzer ein, sich anzumelden und ein Manager dieses Unternehmens zu werden." add_unregistered_user: "Fügen Sie einen nicht registrierten Benutzer hinzu" email_confirmed: "E-Mail bestätigt" @@ -729,9 +729,9 @@ de_DE: edit_profile: Einstellungen properties: Eigenschaften payment_methods: Zahlungsarten - payment_methods_tip: Dieses Unternehmen hat keine Zahlungsmethoden - shipping_methods: Liefermethoden - shipping_methods_tip: Dieses Unternehmen hat Versandmethoden + payment_methods_tip: Dieses Unternehmen hat keine Zahlungsarten + shipping_methods: Lieferarten + shipping_methods_tip: Dieses Unternehmen hat Lieferarten enterprise_fees: Unternehmensgebühren enterprise_fees_tip: Dieses Unternehmen hat keine Gebühren admin_index: @@ -740,47 +740,47 @@ de_DE: sells: vertreibt visible: Sichtbar? owner: Inhaber - producer: Produzent + producer: Erzeuger change_type_form: producer_profile: Erzeuger Profil connect_ofn: Verbindung über OFN herstellen - always_free: IMMER FREI - producer_description_text: Fügen Sie Ihre Produkte zu Open Food Network hinzu, damit Hubs Ihre Produkte in ihren Geschäften lagern können. + always_free: IMMER KOSTENLOS + producer_description_text: Fügen Sie Ihre Produkte zum Open Food Network hinzu, damit Hubs Ihre Produkte in ihren Läden anbieten können. producer_shop: Erzeugerladen sell_your_produce: Verkaufen Sie Ihre eigenen Produkte producer_shop_description_text: Verkaufen Sie Ihre Produkte direkt an Ihre Kunden durch Ihren eigenen Laden im Open Food Network. producer_shop_description_text2: Ein Erzeugerladen ist nur für Ihre eigenen Produkte bestimmt. Wenn Sie Produkte anderer verkaufen möchten, wählen Sie "Hub". - producer_hub: Produzent Hub - producer_hub_text: Verkaufe Produkte von dir selbst und anderen + producer_hub: Hub + producer_hub_text: Verkaufen Sie eigene Produkte und Produkte anderer producer_hub_description_text: Ihr Unternehmen ist das Rückgrat Ihres lokalen Lebensmittelsystems. Sie können sowohl Ihre eigenen Produkte, als auch Produkte von anderen Unternehmen über Ihren Laden im Open Food Network verkaufen. - profile: Profil nur - get_listing: Holen Sie sich einen Eintrag - profile_description_text: Menschen können Sie im Open Food Network finden und kontaktieren. Ihr Unternehmen wird auf der Karte angezeigt und kann in den Suchergebnissen durchsucht werden. + profile: Nur Profil + get_listing: Erstellen Sie einen Eintrag + profile_description_text: Sie können im Open Food Network gefunden und kontaktiert werden. Ihr Unternehmen wird auf der Karte und in Suchergebnissen angezeigt. hub_shop: Hub - hub_shop_text: Verkaufe Produkte von anderen + hub_shop_text: Verkaufen Sie Produkte anderer hub_shop_description_text: Ihr Unternehmen ist das Rückgrat Ihres lokalen Lebensmittelsystems. Sie aggregieren Produkte von anderen Unternehmen und verkaufen sie über Ihren Laden im Open Food Network. - choose_option: Bitte wähle eine der oben genannten Optionen. + choose_option: Bitte wählen Sie eine der oben aufgeführten Optionen. change_now: Jetzt ändern enterprise_user_index: - loading_enterprises: LADEN VON UNTERNEHMEN + loading_enterprises: UNTERNEHMEN WERDEN GELADEN no_enterprises_found: Keine Unternehmen gefunden. - search_placeholder: Suche mit Name + search_placeholder: Suche nach Name manage: Verwalten manage_link: Einstellungen new_form: owner: Inhaber - owner_tip: Der primäre Benutzer, der für dieses Unternehmen verantwortlich ist. - i_am_producer: Ich bin ein Produzent + owner_tip: Der Hauptnutzer, der für dieses Unternehmen verantwortlich ist. + i_am_producer: Ich bin ein Erzeuger contact_name: Kontaktname edit: - editing: 'Die Einstellungen:' + editing: 'Einstellungen:' back_link: Zurück zur Unternehmensliste new: - title: Neue Unternehmung + title: Neues Unternehmen back_link: Zurück zur Unternehmensliste welcome: - welcome_title: Willkommen im Open Food Netzwerk! - welcome_text: Sie haben erfolgreich eine erstellt + welcome_title: Willkommen im Open Food Network! + welcome_text: 'Erfolgreich erstellt:' next_step: Nächster Schritt choose_starting_point: 'Wählen Sie Ihr Paket:' invite_manager: @@ -790,18 +790,18 @@ de_DE: edit: advanced_settings: Erweiterte Einstellungen update_and_close: Aktualisieren und schließen - choose_products_from: 'Wählen Sie Produkte aus:' + choose_products_from: 'Wählen Sie Produkte von:' exchange_form: pickup_time_tip: Wenn Bestellungen von diesem OC für den Kunden bereit sind - pickup_instructions_placeholder: "Abholungsanweisungen" - pickup_instructions_tip: Diese Anweisungen werden Kunden nach Abschluss einer Bestellung angezeigt + pickup_instructions_placeholder: "Abholungsinformationen" + pickup_instructions_tip: Diese Informationen werden Kunden nach Abschluss einer Bestellung angezeigt pickup_time_placeholder: "Bereit für (dh Datum / Uhrzeit)" - receival_instructions_placeholder: "Empfangsanweisungen" + receival_instructions_placeholder: "Lieferinformation" add_fee: 'Gebühr hinzufügen' selected: 'ausgewählt' add_exchange_form: add_supplier: 'Anbieter hinzufügen' - add_distributor: 'Händler hinzufügen' + add_distributor: 'Verteiler hinzufügen' advanced_settings: title: Erweiterte Einstellungen choose_product_tip: Sie können alle verfügbaren Produkte (sowohl eingehende als auch ausgehende) auf nur diejenigen im Katalog von %{inventory} beschränken. @@ -812,18 +812,18 @@ de_DE: add: Koordinatorgebühr hinzufügen form: incoming: Eingehend - supplier: Zulieferer - receival_details: Empfangsdetails + supplier: Anbieter + receival_details: Lieferinformation fees: Gebühren - outgoing: Ausgehende + outgoing: Ausgehend distributor: Verteiler products: Produkte tags: Stichworte add_a_tag: Füge einen Tag hinzu - delivery_details: Abhol- / Lieferdetails + delivery_details: Abhol- / Lieferinformationen debug_info: Debug-Informationen index: - involving: Einbeziehen + involving: Involviert schedule: Zeitplan schedules: Termine adding_a_new_schedule: Hinzufügen eines neuen Zeitplans @@ -844,28 +844,28 @@ de_DE: coordinator: Koordinator orders_close: Bestellungen schließen row: - suppliers: Lieferanten - distributors: Händler + suppliers: Anbieter + distributors: Verteiler variants: Varianten simple_form: - ready_for: Fertig am - ready_for_placeholder: Terminzeit - customer_instructions: Kunden Anweisungen + ready_for: Bereit am + ready_for_placeholder: Datum / Uhrzeit + customer_instructions: Kundeninformation customer_instructions_placeholder: Abhol- oder Lieferscheine products: Produkte fees: Gebühren destroy_errors: - orders_present: Dieser Bestellzyklus wurde von einem Kunden ausgewählt und kann nicht gelöscht werden. Um zu verhindern, dass Kunden darauf zugreifen, schließen Sie es stattdessen. + orders_present: Dieser Bestellzyklus wurde von einem Kunden ausgewählt und kann nicht gelöscht werden. Um weitere Verwendung zu verhindern, können Sie ihn stattdessen schließen. schedule_present: Dieser Bestellzyklus ist mit einem Zeitplan verknüpft und kann nicht gelöscht werden. Bitte heben Sie die Verknüpfung auf oder löschen Sie den Zeitplan zuerst. bulk_update: no_data: Hm, etwas ist schief gelaufen. Keine Bestellzyklusdaten gefunden. date_warning: - msg: Der Bestellvorgang verweist auf (...) noch offene, aber bereits gezeichnete Bestellungen. Dieses Datum zu ändern wird keine der bereits getätigten Bestellungen berühren, sollte wenn möglich aber vermieden werden. Sind Sie sicher, daß Sie fortfahren wollen? - cancel: Abrechen + msg: Dieser Bestellzyklus enthält %{n} offene Abonementbestellungen. Das Ändern des Datums wird bereits erteilte Bestellungen nicht beeinträchtigen, sollte wenn möglich aber vermieden werden. Sind Sie sicher, daß Sie fortfahren wollen? + cancel: Abbrechen proceed: Fortfahren producer_properties: index: - title: Herstellereigenschaften + title: Erzeugereigenschaften proxy_orders: cancel: could_not_cancel_the_order: Die Bestellung konnte nicht storniert werden @@ -876,68 +876,68 @@ de_DE: user_guide: Benutzerhandbuch overview: enterprises_header: - ofn_with_tip: Unternehmen sind Hersteller und / oder Hubs und sind die grundlegende Organisationseinheit innerhalb des Open Food Network. + ofn_with_tip: Unternehmen sind Erzeuger und / oder Hubs und sind die grundlegende Organisationseinheit innerhalb des Open Food Network. enterprises_hubs_tabs: - has_no_payment_methods: "%{enterprise} hat keine Zahlungsmethoden" + has_no_payment_methods: "%{enterprise} hat keine Zahlungsarten" has_no_shipping_methods: "%{enterprise} hat keine Versandarten" has_no_enterprise_fees: "%{enterprise} hat keine Unternehmensgebühren" enterprise_issues: - create_new: Erstelle neu - resend_email: E-Mail zurücksenden - has_no_payment_methods: "%{enterprise} hat derzeit keine Zahlungsmethoden" + create_new: Erstellen + resend_email: E-Mail erneut senden + has_no_payment_methods: "%{enterprise}hat derzeit keine Zahlungsarten" has_no_shipping_methods: "%{enterprise} hat derzeit keine Versandarten" email_confirmation: "E-Mail-Bestätigung steht aus. Wir haben eine Bestätigungs-E-Mail an %{email} gesendet." not_visible: "%{enterprise} ist nicht sichtbar und kann daher nicht auf der Karte oder in Suchen gefunden werden" reports: hidden: VERSTECKT unitsize: EINHEITSGRÖSSE - total: GESAMT + total: SUMME total_items: GESAMTANZAHL - supplier_totals: Bestellzyklus Lieferantensummen - supplier_totals_by_distributor: Order Cycle Supplier Gesamtsumme von Distributor - totals_by_supplier: Order Cycle Distributor Summen nach Lieferant - customer_totals: Bestellzyklus Kundensummen + supplier_totals: Anbieter-Gesamtsummen + supplier_totals_by_distributor: Anbieter-Gesamtsummen nach Verteiler + totals_by_supplier: Verteiler-Gesamtsummen nach Anbieter + customer_totals: Kunden-Gesamtsummen all_products: Alle Produkte inventory: Aktueller Bestand - lettuce_share: SalatShare + lettuce_share: LettuceShare mailing_list: Mailingliste addresses: Adressen - payment_methods: Zahlungsmethoden Bericht - delivery: Sendebericht + payment_methods: Zahlungsarten Bericht + delivery: Lieferbericht tax_types: Steuerarten tax_rates: Steuersätze - pack_by_customer: Nach Kunde verpacken - pack_by_supplier: Nach Lieferant verpacken + pack_by_customer: Packliste nach Kunde + pack_by_supplier: Packliste nach Anbieter orders_and_distributors: - name: Bestellungen und Händler - description: Bestellungen mit Händlerdetails + name: Bestellungen und Verteiler + description: Bestellungen mit Verteilerdetails bulk_coop: name: Massen-Co-Op - description: Berichte für Bulk-Co-Op-Bestellungen + description: Berichte für Massen-Co-Op-Bestellungen payments: name: Zahlungsberichte description: Berichte für Zahlungen orders_and_fulfillment: - name: Bestellungen & Erfüllungsberichte + name: Bestellungs- & Erfüllungsberichte customers: name: Kunden products_and_inventory: name: Produkte und Katalog sales_total: - name: Verkäufe insgesamt + name: Gesamtumsatz description: Gesamtumsatz für alle Bestellungen users_and_enterprises: name: Benutzer und Unternehmen description: Unternehmenseigentum & Status order_cycle_management: - name: Auftragszyklus-Management + name: Bestellzyklus-Verwaltung sales_tax: - name: Mehrwertsteuer + name: Steuern xero_invoices: name: Xero Rechnungen description: Rechnungen für den Import in Xero packing: - name: Verpackungsberichte + name: Packberichte subscriptions: subscriptions: Abonnements new: Neues Abonnement @@ -945,25 +945,25 @@ de_DE: index: please_select_a_shop: Bitte wählen Sie einen Laden edit_subscription: Abonnement bearbeiten - pause_subscription: Abo pausieren - unpause_subscription: Abonnement aufheben + pause_subscription: Abonement pausieren + unpause_subscription: Abonnement fortsetzen cancel_subscription: Abonnement beenden setup_explanation: - just_a_few_more_steps: 'Noch ein paar Schritte bevor Sie beginnen können:' + just_a_few_more_steps: 'Nur noch ein paar Schritte bevor Sie beginnen können:' enable_subscriptions: "Aktivieren Sie Abonnements für mindestens einen Ihrer Läden" enable_subscriptions_step_1_html: 1. Gehen Sie zur Seite %{enterprises_link}, suchen Sie Ihren Laden und klicken Sie auf "Verwalten" enable_subscriptions_step_2: 2. Aktivieren Sie unter "Ladeneinstellungen" die Option Abonnements - set_up_shipping_and_payment_methods_html: Richten Sie die Methoden %{shipping_link} und %{payment_link} ein - set_up_shipping_and_payment_methods_note_html: Beachten Sie, dass nur Bar- und Stripe-Zahlungsmethoden für Abonnements verwendet werden dürfen - ensure_at_least_one_customer_html: Stellen Sie sicher, dass mindestens eine %{customer_link} vorhanden ist + set_up_shipping_and_payment_methods_html: Erstellen Sie %{shipping_link}und %{payment_link} + set_up_shipping_and_payment_methods_note_html: Beachten Sie, dass nur Bar- und Stripe-Zahlungsarten für Abonnements verwendet werden dürfen + ensure_at_least_one_customer_html: Stellen Sie sicher, dass mindestens ein %{customer_link} vorhanden ist create_at_least_one_schedule: Erstellen Sie mindestens einen Zeitplan create_at_least_one_schedule_step_1_html: 1. Gehen Sie auf die Seite %{order_cycles_link} create_at_least_one_schedule_step_2: 2. Erstellen Sie einen Bestellzyklus, falls Sie dies noch nicht getan haben create_at_least_one_schedule_step_3: 3. Klicken Sie auf "+ Neuer Zeitplan" und füllen Sie das Formular aus once_you_are_done_you_can_html: Sobald Sie fertig sind, können Sie %{reload_this_page_link} - reload_this_page: lade diese Seite neu + reload_this_page: diese Seite neu laden steps: - details: 1. Grundlegende Details + details: 1. Grundlegendes address: 2. Adresse products: 3. Fügen Sie Produkte hinzu review: 4. Überprüfen und speichern @@ -974,36 +974,36 @@ de_DE: details: details: Einzelheiten invalid_error: Hoppla! Bitte füllen Sie alle erforderlichen Felder aus ... - allowed_payment_method_types_tip: Zurzeit können nur Bar- und Stripe-Zahlungsmethoden verwendet werden + allowed_payment_method_types_tip: Zurzeit können nur Bar- und Stripe-Zahlungsarten verwendet werden credit_card: Kreditkarte - charges_not_allowed: Aufträge dieses Kunden/dieser Kundin sind nicht gestattet. - no_default_card: Der Kunde/die Kundin hat keine Karte, die aufgeladen werden kann. - card_ok: Der Kunde/die Kundin hat eine Karte, die aufgeladen werden kann. + charges_not_allowed: Der Kunde hat keine Belastungen erlaubt. + no_default_card: Für den Kunden ist keine belastbare Karte vorhanden. + card_ok: Eine belastbare Karte ist für den Kunden vorhanden. loading_flash: - loading: LADEN VON ABONNEMENTS + loading: ABONNEMENTS WERDEN GELADEN review: details: Einzelheiten address: Adresse products: Produkte - product_already_in_order: Dieses Produkt wurde bereits zur Bestellung hinzugefügt. Bitte bearbeiten Sie die Menge direkt. + product_already_in_order: Dieses Produkt wurde bereits zur Bestellung hinzugefügt. Bitte änderns Sie stattdessen die Menge. orders: number: Nummer confirm_edit: Sind Sie sicher, dass Sie diese Bestellung bearbeiten möchten? Dies kann die automatische Synchronisierung von Änderungen am Abonnement in Zukunft erschweren. - confirm_cancel_msg: Sind Sie sicher, dass Sie dieses Abonnement kündigen möchten? Diese Aktion kann nicht rückgängig gemacht werden. + confirm_cancel_msg: Sind Sie sicher, dass Sie dieses Abonnement kündigen möchten? Dieser Vorgang kann nicht rückgängig gemacht werden. cancel_failure_msg: 'Entschuldigung, Stornierung fehlgeschlagen!' confirm_pause_msg: Möchten Sie dieses Abonnement wirklich pausieren? - pause_failure_msg: 'Entschuldigung, die Pause ist fehlgeschlagen!' - confirm_unpause_msg: Möchten Sie dieses Abonnement wirklich wieder aktivieren? - unpause_failure_msg: 'Entschuldigung, fehlgeschlagen!' - confirm_cancel_open_orders_msg: "Einige Bestellungen für dieses Abonnement sind derzeit geöffnet. Der Kunde wurde bereits darüber informiert, dass die Bestellung aufgegeben wird. Möchten Sie diese Bestellung (en) stornieren oder behalten?" - resume_canceled_orders_msg: "Einige Bestellungen für dieses Abonnement können jetzt fortgesetzt werden. Sie können sie aus dem Dropdown-Menü für Bestellungen wiederherstellen." - yes_cancel_them: Annulliere sie - no_keep_them: Behalte sie + pause_failure_msg: 'Entschuldigung, Pausieren fehlgeschlagen!' + confirm_unpause_msg: Möchten Sie dieses Abonnement wirklich fortsetzen? + unpause_failure_msg: 'Entschuldigung, Fortsetzung fehlgeschlagen!' + confirm_cancel_open_orders_msg: "Einige Bestellungen für dieses Abonnement sind derzeit offen. Der Kunde wurde bereits informiert, dass die Bestellung aufgegeben wird. Möchten Sie diese Bestellung(en) stornieren oder erhalten?" + resume_canceled_orders_msg: "Manche Bestellungen für dieses Abonnement können jetzt fortgesetzt werden. Sie können sie aus dem Bestellungen-Dropdown-Menü wiederherstellen." + yes_cancel_them: Stornieren + no_keep_them: Behalten yes_i_am_sure: Ja, ich bin mir sicher - order_update_issues_msg: Einige Bestellungen konnten nicht automatisch aktualisiert werden, dies liegt wahrscheinlich daran, dass sie manuell bearbeitet wurden. Bitte überprüfen Sie die unten aufgeführten Punkte und nehmen Sie gegebenenfalls Anpassungen an einzelnen Bestellungen vor. + order_update_issues_msg: Einige Bestellungen konnten nicht automatisch aktualisiert werden. Dies liegt wahrscheinlich daran, dass sie manuell bearbeitet wurden. Bitte überprüfen Sie die unten aufgeführten Punkte und nehmen Sie gegebenenfalls Anpassungen an einzelnen Bestellungen vor. no_results: no_subscriptions: Noch keine Abonnements - why_dont_you_add_one: Warum fügst du keinen hinzu? :) + why_dont_you_add_one: Vielleicht welche hinzufügen? :) no_matching_subscriptions: Keine passenden Abonnements gefunden schedules: destroy: @@ -1014,24 +1014,51 @@ de_DE: stripe_connect_success: "Stripe-Konto erfolgreich verbunden" stripe_connect_fail: Die Verbindung Ihres Stripe-Kontos ist fehlgeschlagen stripe_connect_settings: - resource: Streifenverbindungskonfiguration + resource: Konfiguration für Stripe Connect checkout: already_ordered: - cart: "Wagen" - message_html: "Sie haben bereits eine Bestellung für diesen Bestellzyklus. Überprüfen Sie die %{cart}, um die Artikel zu sehen, die Sie zuvor bestellt haben. Sie können Artikel auch stornieren, solange der Bestellzyklus geöffnet ist." + cart: "Warenkorb" + message_html: "Sie haben bereits eine Bestellung für diesen Bestellzyklus. Überprüfen Sie den %{cart}, um die Artikel zu sehen, die Sie zuvor bestellt haben. Sie können Artikel auch stornieren, solange der Bestellzyklus geöffnet ist." shops: hubs: show_closed_shops: "Geschlossene Läden anzeigen" hide_closed_shops: "Geschlossene Läden ausblenden." - show_on_map: "Zeige alle auf der Karte" + show_on_map: "Alle auf der Karte zeigen" shared: menu: cart: - checkout: "Checke jetzt aus" - already_ordered_products: "Bereits in dieser Reihenfolge bestellt" + checkout: "Zur Kasse" + already_ordered_products: "Bereits in diesem Bestellzyklus bestellt" register_call: selling_on_ofn: "Interesse am Open Food Network?" register: "Hier anmelden" + footer: + footer_global_headline: "OFN Global" + footer_global_home: "Startseite" + footer_global_news: "Aktuelles" + footer_global_about: "Über Uns" + footer_global_contact: "Kontakt" + footer_sites_headline: "OFN Webseiten" + footer_sites_developer: "Entwickler" + footer_sites_community: "Community" + footer_sites_userguide: "Benutzerhandbuch" + footer_secure: "Sicher und vertrauenswürdig." + footer_secure_text: "Open Food Network verwendet überall SSL-Verschlüsselung (2048 Bit RSA), um Ihre Einkaufs- und Zahlungsinformationen geheim zu halten. Unsere Server speichern Ihre Kreditkartendetails nicht und Zahlungen werden von PCI-konformen Dienstleistern verarbeitet." + footer_contact_headline: "In Verbindung bleiben" + footer_contact_email: "Schreiben Sie uns eine E-Mail" + footer_nav_headline: "Navigieren" + footer_join_headline: "Mitmachen" + footer_join_body: "Erstellen Sie einen Eintrag, einen Laden oder ein Gruppenverzeichnis im Open Food Network." + footer_join_cta: "Erzähl mir mehr!" + footer_legal_call: "Lesen Sie unsere" + footer_legal_tos: "AGB" + footer_legal_visit: "Finden Sie uns auf" + footer_legal_text_html: "Open Food Network ist eine kostenlose Open-Source Software-Plattform. Unser Inhalt ist mit %{content_license} und unser Code mit %{code_license} lizenziert." + footer_data_text_with_privacy_policy_html: "Wir passen auf Ihre Daten auf. Siehe unsere %{privacy_policy} und %{cookies_policy}" + footer_data_text_without_privacy_policy_html: "Wir passen auf Ihre Daten auf. Siehe unsere %{cookies_policy}" + footer_data_privacy_policy: "Datenschutz-Bestimmungen" + footer_data_cookies_policy: "Cookie-Richtlinien" + footer_skylight_dashboard_html: Leistungsdaten sind in der %{dashboard} verfügbar. shop: messages: login: "Anmeldung" @@ -1051,11 +1078,11 @@ de_DE: invoice_column_unit_price_with_taxes: "Stückpreis (inkl. Steuern)" invoice_column_unit_price_without_taxes: "Stückpreis (zzgl. Steuern)" invoice_column_price_with_taxes: "Gesamtpreis (inkl. Steuern)" - invoice_column_price_without_taxes: "Gesamtpreis (ohne Steuern)" + invoice_column_price_without_taxes: "Gesamtpreis (zzgl. Steuern)" invoice_column_tax_rate: "Steuersatz" invoice_tax_total: "Gesamtkosten:" tax_invoice: "Steuerrechnung" - tax_total: "Gesamtsteuer (%{rate}):" + tax_total: "Steuersumme (%{rate}):" total_excl_tax: "Summe (ohne Steuern):" total_incl_tax: "Gesamt (Inkl. Steuern):" abn: "ABN:" @@ -1065,24 +1092,30 @@ de_DE: date_of_transaction: "Datum der Transaktion:" ticket_column_qty: "Menge" ticket_column_item: "Artikel" - ticket_column_unit_price: "Einzelpreis" + ticket_column_unit_price: "Stückpreis" ticket_column_total_price: "Gesamtpreis" + menu_1_title: "Läden" + menu_2_title: "Karte" + menu_3_title: "Erzeuger" + menu_4_title: "Gruppen" + menu_5_title: "Über Uns" + menu_6_title: "Verbinde" + menu_7_title: "Lerne" logo: "Logo (640x130)" - logo_mobile: "Mobiles Logo (75x26)" - logo_mobile_svg: "Mobiles Logo (SVG)" + logo_mobile: "Handylogo (75x26)" + logo_mobile_svg: "Handylogo (SVG)" home_hero: "Heldenbild" home_show_stats: "Zeige Statistiken" footer_logo: "Logo (220x76)" footer_facebook_url: "Facebook URL" - footer_twitter_url: "Twitter-URL" + footer_twitter_url: "Twitter URL" footer_instagram_url: "Instagram URL" - footer_linkedin_url: "LinkedIn-URL" - footer_googleplus_url: "Google Plus-URL" - footer_pinterest_url: "Pinterest-URL" + footer_linkedin_url: "LinkedIn URL" + footer_googleplus_url: "Google Plus URL" + footer_pinterest_url: "Pinterest URL" footer_email: "E-Mail:" footer_links_md: "Links" footer_about_url: "Über URL" - footer_tos_url: "Nutzungsbedingungen URL" name: Name first_name: Vorname last_name: Nachname @@ -1093,26 +1126,26 @@ de_DE: address_placeholder: z.B. Gartenstrasse 123 address2: Adresse (Fortsetzung) city: Ort - city_placeholder: z.B. Northcote + city_placeholder: z.B. Nordwestheim postcode: Postleitzahl - postcode_placeholder: z.B. 3070 + postcode_placeholder: z.B. 30701 state: Bundesland country: Land unauthorized: Nicht autorisiert - terms_of_service: "Geschäftsbedingungen" + terms_of_service: "AGB" on_demand: Auf Anfrage none: Keine not_allowed: Nicht erlaubt - no_shipping: keine Versandarten - no_payment: keine Zahlungsmethoden - no_shipping_or_payment: keine Versand- oder Zahlungsmethoden + no_shipping: keine Lieferarten + no_payment: keine Zahlungsarten + no_shipping_or_payment: keine Versand- oder Zahlungsarten unconfirmed: unbestätigt days: Tage label_shop: "Laden" label_shops: "Läden" label_map: "Karte" - label_producer: "Hersteller" - label_producers: "Produzenten" + label_producer: "Erzeuger" + label_producers: "Erzeuger" label_groups: "Gruppen" label_about: "Über" label_connect: "Verbinde" @@ -1120,21 +1153,21 @@ de_DE: label_blog: "Blog" label_support: "Unterstützung" label_shopping: "Einkauf" - label_login: "Login" - label_logout: "Logout" + label_login: "Anmeldung" + label_logout: "Ausloggen" label_signup: "Registrieren" - label_administration: "Administration" - label_admin: "Administrator" + label_administration: "Verwaltung" + label_admin: "Verwalter" label_account: "Konto" label_more: "Mehr anzeigen" label_less: "Weniger anzeigen" - label_notices: "Notizen" + label_notices: "Bemerkungen" cart_items: "Artikel" cart_headline: "Ihr Warenkorb" total: "Total" - cart_updating: "Einkaufswagen aktualisieren..." - cart_empty: "Leerer Einkaufswagen" - cart_edit: "Einkaufswagen bearbeiten" + cart_updating: "Warenkorb aktualisieren..." + cart_empty: "Warenkorb leer" + cart_edit: "Warenkorb bearbeiten" card_number: Kartennummer card_securitycode: "Sicherheitscode" card_expiry_date: Ablaufdatum @@ -1142,42 +1175,23 @@ de_DE: card_expiry_abbreviation: "Exp" new_credit_card: "Neue Kreditkarte" my_credit_cards: Meine Kreditkarten - add_new_credit_card: Fügen Sie eine neue Kreditkarte hinzu + add_new_credit_card: Neue Karte hinzufügen saved_cards: Gespeicherte Karten - add_a_card: Füge eine Karte hinzu + add_a_card: Karte hinzufügen add_card: Karte hinzufügen - you_have_no_saved_cards: Du hast noch keine Karten gespeichert + you_have_no_saved_cards: Sie haben noch keine Karten gespeichert saving_credit_card: Kreditkarte speichern ... card_has_been_removed: "Ihre Karte wurde entfernt (Nummer: %{number})" - card_could_not_be_removed: Entschuldigung, die Karte konnte nicht entfernt werden + card_could_not_be_removed: Die Karte konnte nicht entfernt werden ie_warning_headline: "Ihr Browser ist veraltet :-(" ie_warning_text: "Für das beste Open-Food-Network-Erlebnis empfehlen wir dringend, Ihren Browser zu aktualisieren:" ie_warning_chrome: Chrome herunterladen - ie_warning_firefox: Firefox herunterlade + ie_warning_firefox: Firefox herunterladen ie_warning_ie: Internet Explorer aktualisieren - ie_warning_other: "Können Sie Ihren Browser nicht aktualisieren? Testen Sie Open Food Network auf Ihrem Smartphone :-)" - footer_global_headline: "OFN Global" - footer_global_home: "Zuhause" - footer_global_news: "Nachrichten" - footer_global_about: "Über" - footer_global_contact: "Kontakt" - footer_sites_headline: "OFN Seiten" - footer_sites_developer: "Entwickler" - footer_sites_community: "Community" - footer_sites_userguide: "Benutzerhandbuch" - footer_secure: "Sicher und vertrauenswürdig." - footer_secure_text: "Open Food Network verwendet überall SSL-Verschlüsselung (2048 Bit RSA), um Ihre Einkaufs- und Zahlungsinformationen geheim zu halten. Unsere Server speichern Ihre Kreditkartendetails nicht und Zahlungen werden von PCI-konformen Dienstleistern verarbeitet." - footer_contact_headline: "Den Kontakt halten" - footer_contact_email: "Schreiben Sie uns eine E-Mail" - footer_nav_headline: "Navigieren" - footer_join_headline: "Begleiten Sie uns" - footer_join_body: "Erstellen Sie einen Eintrag, ein Geschäft oder ein Gruppenverzeichnis im Open Food Network." - footer_join_cta: "Erzähl mir mehr!" - footer_legal_call: "Lesen Sie unsere" - footer_legal_tos: "Geschäftsbedingungen" - footer_legal_visit: "Finden Sie uns auf" - footer_legal_text_html: "Open Food Network ist eine freie und Open-Source-Software-Plattform. Unser Inhalt ist mit %{content_license} und unserem Code mit %{code_license} lizenziert." - footer_skylight_dashboard_html: Leistungsdaten sind unter %{dashboard} verfügbar. + ie_warning_other: "Können Sie Ihren Browser nicht aktualisieren? Versuchen Sie Open Food Network auf Ihrem Smartphone :-)" + legal: + cookies_banner: + cookies_policy_link: "Hinweise zu Cookies" home_shop: Jetzt einkaufen brandstory_headline: "Essen, ohne eigene Rechtspersönlichkeit." brandstory_intro: "Manchmal ist der beste Weg, das System zu reparieren, ein neues zu starten ..." @@ -1205,13 +1219,13 @@ de_DE: stats_shops: "Lebensmittelgeschäfte" stats_shoppers: "Lebensmittelkäufer" stats_orders: "Essen Bestellungen" - checkout_title: Auschecken - checkout_now: Checke jetzt aus + checkout_title: Kasse + checkout_now: Zur Kasse checkout_order_ready: Bestellung bereit für checkout_hide: Verstecke checkout_expand: Erweitern - checkout_headline: "Ok, bereit zur Kasse?" - checkout_as_guest: "Kasse als Gast" + checkout_headline: "Ok, jetzt zur Kasse?" + checkout_as_guest: "Als Gast zur Kasse" checkout_details: "Deine Details" checkout_billing: "Rechnungsinfo" checkout_default_bill_address: "Als Standard-Rechnungsadresse speichern" @@ -1259,8 +1273,8 @@ de_DE: email_registered: "ist jetzt Teil von" email_userguide_html: "Das Benutzerhandbuch mit ausführlicher Unterstützung für die Einrichtung Ihres Producer oder Hub finden Sie hier: %{link}" email_admin_html: "Sie können Ihr Konto verwalten, indem Sie sich bei %{link} anmelden oder indem Sie oben rechts auf der Startseite auf das Zahnrad klicken und Administration auswählen." - email_community_html: "Wir haben auch ein Online-Forum für Community-Diskussionen in Bezug auf OFN-Software und die einzigartigen Herausforderungen eines Lebensmittelunternehmens. Sie werden ermutigt mitzumachen. Wir entwickeln uns ständig weiter und Ihr Beitrag in diesem Forum wird prägen, was als nächstes passiert. %{link}" - join_community: "Trete der Community bei" + email_community_html: "Wir haben auch ein Online-Forum für Community-Diskussionen in Bezug auf OFN-Software und die einzigartigen Herausforderungen eines Lebensmittelunternehmens. Reden Sie doch mit. Wir entwickeln uns ständig weiter und Ihr Beitrag in diesem Forum prägt, was als nächstes passiert. %{link}" + join_community: "Treten Sie der Community bei" email_confirmation_activate_account: "Bevor wir Ihr neues Konto aktivieren können, müssen wir Ihre E-Mail-Adresse bestätigen." email_confirmation_greeting: "Hallo, %{contact}!" email_confirmation_profile_created: "Ein Profil für %{name} wurde erfolgreich erstellt! Um Ihr Profil zu aktivieren, müssen wir diese E-Mail-Adresse bestätigen." @@ -1494,7 +1508,7 @@ de_DE: orders_edit_headline: Ihr Warenkorb orders_edit_time: Bestellung bereit für orders_edit_continue: Mit dem Einkauf fortfahren - orders_edit_checkout: Auschecken + orders_edit_checkout: Kasse orders_form_empty_cart: "Einkaufskorb leeren" orders_form_subtotal: Zwischensumme erzeugen orders_form_admin: Admin & Handhabung @@ -1750,7 +1764,7 @@ de_DE: registration_detail_state_error: "Staat erforderlich" registration_detail_country: "Land:" registration_detail_country_error: "Bitte wähle ein Land" - shipping_method_destroy_error: "Diese Versandmethode kann nicht gelöscht werden, da sie in einer Bestellung referenziert wird: %{number}." + shipping_method_destroy_error: "Diese Lieferart kann nicht gelöscht werden, da sie in einer Bestellung verwendet wird: %{number}." accounts_and_billing_task_already_running_error: "Eine Aufgabe wird bereits ausgeführt. Bitte warten Sie, bis sie abgeschlossen ist" accounts_and_billing_start_task_notice: "Aufgabe in Warteschlange gestellt" fees: "Gebühren" @@ -1815,7 +1829,7 @@ de_DE: close: "Abschließen" create: "Neu" search: "Suche" - supplier: "Zulieferer" + supplier: "Anbieter" product_name: "Produktname" product_description: "Produktbeschreibung" units: "Einheitsgröße" @@ -1824,7 +1838,7 @@ de_DE: enterprise_fees: "Unternehmensgebühren" process_my_order: "Verarbeite meine Bestellung" delivery_instructions: Lieferanleitungen - delivery_method: Zustellungsmethode + delivery_method: Lieferart fee_type: "Art der Gebühr" tax_category: "Steuerkategorie" calculator: "Rechner" @@ -1864,7 +1878,7 @@ de_DE: spree_admin_overview_enterprises_footer: "VERWALTEN SIE MEINE UNTERNEHMEN" spree_admin_enterprises_hubs_name: "Name" spree_admin_enterprises_create_new: "neu erstellen" - spree_admin_enterprises_shipping_methods: "Lieferoptionen" + spree_admin_enterprises_shipping_methods: "Lieferarten" spree_admin_enterprises_fees: "Unternehmensgebühren" spree_admin_enterprises_none_create_a_new_enterprise: "ERSTELLEN SIE EIN NEUES UNTERNEHMEN" spree_admin_enterprises_none_text: "Sie haben noch keine Unternehmen" @@ -1880,7 +1894,7 @@ de_DE: spree_admin_unit_description: Einheit Beschreibung spree_admin_variant_unit: Varianteneinheit spree_admin_variant_unit_scale: Variationseinheitsskala - spree_admin_supplier: Lieferant + spree_admin_supplier: Anbieter spree_admin_product_category: Produktkategorie spree_admin_variant_unit_name: Name der Varianteinheit change_package: "Paket ändern" @@ -1909,13 +1923,12 @@ de_DE: edit_profile_details_etc: "Ändern Sie Ihre Profilbeschreibung, Bilder usw." order_cycle: "Bestellungszyklus" order_cycles: "Bestellrunden" + enterprise_relationships: "Unternehmensberechtigungen" remove_tax: "Steuer entfernen" - enterprise_terms_of_service: "Unternehmens-Nutzungsbedingungen" - enterprises_require_tos: "Unternehmen müssen die Nutzungsbedingungen akzeptieren" - enterprise_tos_link: "Links zum Unternehmens-Servicebedingungen" + enterprise_tos_link: "Link zu den AGB des Unternehmens" enterprise_tos_message: "Wir wollen mit Menschen zusammenarbeiten, die unsere Ziele und Werte teilen. Als solche bitten wir neue Unternehmen, uns zuzustimmen" - enterprise_tos_link_text: "Nutzungsbedingungen." - enterprise_tos_agree: "Ich stimme den oben genannten Nutzungsbedingungen zu" + enterprise_tos_link_text: "AGB." + enterprise_tos_agree: "Ich stimme den oben genannten Geschäftsbedingungen zu" tax_settings: "Steuereinstellungen" products_require_tax_category: "Produkte benötigen eine Steuerkategorie" admin_shared_address_1: "Adresse" @@ -1932,7 +1945,7 @@ de_DE: hub_sidebar_red: "rot" shop_trial_in_progress: "Ihr Laden-Versuch läuft in %{days} ab." report_customers_distributor: "Verteiler" - report_customers_supplier: "Zulieferer" + report_customers_supplier: "Anbieter" report_customers_cycle: "Bestellungszyklus" report_customers_type: "Berichtsart" report_customers_csv: "Download als CSV" @@ -1970,7 +1983,7 @@ de_DE: report_header_paid: Bezahlt? report_header_delivery: Lieferung? report_header_shipping: Lieferung - report_header_shipping_method: Liefermethode + report_header_shipping_method: Lieferart report_header_shipping_instructions: Versand-Anweisungen report_header_ship_street: Schiffsstraße report_header_ship_street_2: Schiffsstraße 2 @@ -2012,7 +2025,7 @@ de_DE: report_header_unallocated: Nicht zugewiesen report_header_max_quantity_excess: Max Menge Überschuss report_header_taxons: Taxonen - report_header_supplier: Zulieferer + report_header_supplier: Anbieter report_header_producer: Erzeuger report_header_producer_suburb: Produzent Vorort report_header_unit: Einheit @@ -2115,7 +2128,7 @@ de_DE: business_details: "Geschäftsdetails" properties: "Eigenschaften" shipping: "Versand" - shipping_methods: "Lieferoptionen" + shipping_methods: "Lieferart" payment_methods: "Zahlungsarten" payment_method_fee: "Transaktionsgebühr" inventory_settings: "Katalogeinstellungen" @@ -2131,7 +2144,7 @@ de_DE: content_configuration_pricing_table: "(TODO: Preistabelle)" content_configuration_case_studies: "(TODO: Fallstudien)" content_configuration_detail: "(Todo: Detail)" - enterprise_name_error: "wurde bereits genommen. Wenn dies Ihr Unternehmen ist und Sie die Eigentumsrechte beanspruchen möchten oder wenn Sie mit diesem Unternehmen handeln möchten, wenden Sie sich bitte an den aktuellen Manager dieses Profils unter %{email}." + enterprise_name_error: "wurde bereits vergeben. Wenn dies Ihr Unternehmen ist und Sie die Eigentumsrechte beanspruchen möchten oder wenn Sie mit diesem Unternehmen handeln möchten, wenden Sie sich bitte an den aktuellen Manager dieses Profils unter %{email}." enterprise_owner_error: "^ %{email} darf keine weiteren Unternehmen besitzen (Limit ist %{enterprise_limit})." enterprise_role_uniqueness_error: "^ Diese Rolle ist bereits vorhanden." inventory_item_visibility_error: muss wahr oder falsch sein @@ -2145,11 +2158,11 @@ de_DE: adjustments_tax_rate_error: "^ Bitte überprüfen Sie, ob der Steuersatz für diese Anpassung korrekt ist." active_distributors_not_ready_for_checkout_message_singular: >- Das Hub %{distributor_names} ist in einem aktiven Bestellzyklus aufgeführt, - hat jedoch keine gültigen Versand- und Zahlungsmethoden. Bis Sie diese einrichten, + hat jedoch keine gültigen Versand- und Zahlungsarten. Bis Sie diese einrichten, können Kunden nicht an diesem Hub einkaufen. active_distributors_not_ready_for_checkout_message_plural: >- Die Hubs %{distributor_names} sind in einem aktiven Bestellzyklus aufgeführt, - haben jedoch keine gültigen Versand- und Zahlungsmethoden. Bis Sie diese einrichten, + haben jedoch keine gültigen Versand- und Zahlungsarten. Bis Sie diese einrichten, können Kunden nicht an diesen Hubs einkaufen. enterprise_fees_update_notice: Ihre Unternehmensgebühren wurden aktualisiert. enterprise_fees_destroy_error: "Diese Unternehmensgebühr kann nicht gelöscht werden, da sie von einer Produktverteilung referenziert wird: %{id} - %{name}." @@ -2204,7 +2217,7 @@ de_DE: overview: Überblick overview_text: > Mit Tag-Regeln können Sie beschreiben, welche Elemente für welche Kunden - sichtbar sind oder nicht. Artikel können Versandmethoden, Zahlungsmethoden, + sichtbar sind oder nicht. Artikel können Versandarten, Zahlungsarten, Produkte und Bestellzyklen sein. by_default_rules: "\"Standardmäßig ...\" Regeln" by_default_rules_text: > @@ -2247,9 +2260,8 @@ de_DE: Ihr Unternehmen wird nicht vollständig aktiviert, bis ein Paket aus den Optionen auf der linken Seite ausgewählt wird. choose_package_text2: > - Klicken Sie auf eine Option, um detailliertere Informationen zu jedem - Paket zu erhalten, und drücken Sie die rote SAVE-Taste, wenn Sie fertig - sind! + Klicken Sie auf ein Paket, um weitere Informationen zu erhalten. Klicken + Sie SPEICHERN, wenn Sie fertig sind! profile_only: Profil nur profile_only_cost: "KOSTEN: IMMER KOSTENLOS" profile_only_text1: > @@ -2318,6 +2330,9 @@ de_DE: select_rule_type: "Wählen Sie einen Regelart aus:" resend_user_email_confirmation: resend: "Erneut senden" + sending: "Erneut senden..." + done: "Erneut senden ✓" + failed: "Erneut senden fehlgeschlagen ✗" out_of_stock: reduced_stock_available: Reduzierter Bestand verfügbar out_of_stock_text: > @@ -2341,8 +2356,8 @@ de_DE: stock_reset: Aktien werden auf Standardwerte zurückgesetzt. tag_rules: show_hide_variants: 'Produktvarianten im Laden anzeigen oder verbergen' - show_hide_shipping: 'Versandarten im Shop anzeigen' - show_hide_payment: 'Zahlungsmethoden im Shop anzeigen' + show_hide_shipping: 'Lieferarten in der Kasse anzeigen oder verbergen' + show_hide_payment: 'Zahlungsarten an der Kasse anzeigen oder verbergen' show_hide_order_cycles: 'Bestellzyklen in meinem Laden anzeigen order verbergen' visible: SICHTBAR not_visible: UNSICHTBAR @@ -2418,18 +2433,17 @@ de_DE: account_id: Konto-ID business_name: Geschäftsname charges_enabled: Gebühren aktiviert - payments: - source_forms: - stripe: - no_payment_via_admin_backend: Das Erstellen von Stripe-basierten Zahlungen aus dem Admin-Backend ist derzeit nicht möglich products: new: title: 'Neues Produkt' unit_name_placeholder: 'z.B. Trauben' index: + header: + title: Massenbearbeitung von Produkten indicators: title: LADE PRODUKTE no_products: "Bisher sind keine Produkte gewählt worden. Warum fügen Sie nicht einige hinzu?" + no_results: "Entschuldigung, keine Ergebnisse stimmen überein" products_head: name: Name unit: Einheit @@ -2439,6 +2453,10 @@ de_DE: inherits_properties?: Vererbt Eigenschaften available_on: Verfügbar am av_on: "Verfüg. am" + import_date: "Importdatum" + products_variant: + variant_has_n_overrides: "Diese Variante hat %{n} override (s)" + new_variant: "Neue Variante" product_name: Produktname primary_taxon_form: product_category: Produktkategorie @@ -2451,13 +2469,20 @@ de_DE: table: select_and_search: "Wählen Sie Filter und klicken Sie auf SEARCH, um auf Ihre Daten zuzugreifen." bulk_coop: - bulk_coop_supplier_report: 'Bulk Coop - Summen nach Lieferant' + bulk_coop_supplier_report: 'Massen-Kooperative - Summen nach Anbieter' bulk_coop_allocation: 'Massenkoop - Zuteilung' bulk_coop_packing_sheets: 'Massenkoop - Verpackungsblätter' bulk_coop_customer_payments: 'Massenkoop - Kundenzahlungen' + users: + email_confirmation: + confirmation_pending: "E-Mail-Bestätigung steht aus. Wir haben eine Bestätigungs-E-Mail an %{address} gesendet." variants: autocomplete: producer_name: Produzent + general_settings: + edit: + enterprises_require_tos: "Unternehmen müssen die AGB akzeptieren" + footer_tos_url: "AGB URL" checkout: payment: stripe: @@ -2566,6 +2591,6 @@ de_DE: cards: authorised_shops: Bevollmächtigte Läden authorised_shops_popover: Dies ist die Liste der Shops, die Ihre Standardkreditkarte für eventuell vorhandene Abonnements (dh wiederkehrende Bestellungen) belasten dürfen. Ihre Kartendetails werden sicher aufbewahrt und nicht an Ladenbesitzer weitergegeben. Sie werden immer benachrichtigt, wenn Sie belastet werden. - saved_cards_popover: Dies ist die Liste der Karten, die Sie für die spätere Verwendung gespeichert haben. Ihr "Standard" wird automatisch beim Abschließen einer Bestellung ausgewählt und kann von allen Geschäften belastet werden, die Sie dazu berechtigt haben (siehe rechts). + saved_cards_popover: Dies ist die Liste der Karten, die Sie für spätere Verwendung gespeichert haben. Ihr "Standard" wird automatisch beim Abschließen einer Bestellung ausgewählt und kann von allen Geschäften belastet werden, die Sie dazu berechtigt haben (siehe rechts). localized_number: invalid_format: hat ein ungültiges Format. Bitte gebe eine Nummer ein. diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml index d50003b209..f6170ef7d9 100644 --- a/config/locales/en_GB.yml +++ b/config/locales/en_GB.yml @@ -19,7 +19,7 @@ en_GB: email: taken: "There's already an account for this email. Please login or reset your password." spree/order: - no_card: There are no valid credit cards available + no_card: There are no authorised credit cards available to charge order_cycle: attributes: orders_close_at: @@ -41,11 +41,10 @@ en_GB: payment_method: not_available_to_shop: "is not available to %{shop}" invalid_type: "must be a Cash or Stripe method" + charges_not_allowed: "^Credit card charges are not allowed by this customer" + no_default_card: "^No default card available for this customer" shipping_method: not_available_to_shop: "is not available to %{shop}" - credit_card: - not_available: "is not available" - blank: "is required" devise: confirmations: send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." @@ -160,6 +159,7 @@ en_GB: distributors: Distributors distribution: Distribution bulk_order_management: Bulk Order Management + enterprises: Enterprises enterprise_groups: Groups reports: Reports variant_overrides: Inventory @@ -310,6 +310,42 @@ en_GB: included_tax_tip: "The total tax included in the example monthly bill, given the settings and the turnover provided." total_monthly_bill_incl_tax: "Total Monthly Bill (Incl. Tax)" total_monthly_bill_incl_tax_tip: "The example total monthly bill with tax included, given the settings and the turnover provided." + cache_settings: + show: + title: Caching + distributor: Distributor + order_cycle: Order Cycle + status: Status + diff: Diff + error: Error + invoice_settings: + edit: + title: Invoice Settings + invoice_style2?: Use the alternative invoice model that includes total tax breakdown per rate and tax rate info per item (not yet suitable for countries displaying prices excluding tax) + enable_receipt_printing?: Show options for printing receipts using thermal printers in order dropdown? + stripe_connect_settings: + edit: + title: "Stripe Connect" + settings: "Settings" + stripe_connect_enabled: Enable shops to accept payments using Stripe Connect? + no_api_key_msg: No Stripe account exists for this enterprise. + configuration_explanation_html: For detailed instructions on configuring the Stripe Connect integration, please consult this guide. + status: Status + ok: Ok + instance_secret_key: Instance Secret Key + account_id: Account ID + business_name: Business Name + charges_enabled: Charges Enabled + charges_enabled_warning: "Warning: Charges are not enabled for your account" + auth_fail_error: The API key you provided is invalid + empty_api_key_error_html: No Stripe API key has been provided. To set your API key, please follow these instructions + matomo_settings: + edit: + title: "Matomo Settings" + matomo_url: "Matomo URL" + matomo_site_id: "Matomo Site ID" + info_html: "Matomo is a Web and Mobile Analytics. You can either host Matomo on-premises or use a cloud-hosted service. See matomo.org for more information." + config_instructions_html: "Here you can configure the OFN Matomo integration. The Matomo URL below should point to the Matomo instance where the user tracking information will be sent to; if it is left empty, Matomo user tracking will be disabled. The Site ID field is not mandatory but useful if you are tracking more than one website on a single Matomo instance; it can be found on the Matomo instance console." customers: index: add_customer: "Add Customer" @@ -332,16 +368,9 @@ en_GB: update_address: 'Update Address' confirm_delete: 'Sure to delete?' search_by_email: "Search by email/code..." + guest_label: 'Guest checkout' destroy: has_associated_orders: 'Delete failed: customer has associated orders with his shop' - cache_settings: - show: - title: Caching - distributor: Distributor - order_cycle: Order Cycle - status: Status - diff: Diff - error: Error contents: edit: title: Content @@ -350,6 +379,7 @@ en_GB: producer_signup_page: Producer signup page hub_signup_page: Hub signup page group_signup_page: Group signup page + main_links: Main Menu Links footer_and_external_links: Footer and External Links your_content: Your content enterprise_fees: @@ -414,20 +444,29 @@ en_GB: index: select_file: Select a spreadsheet to upload spreadsheet: Spreadsheet - import_into: "Import into:" + choose_import_type: Select import type + import_into: Import type product_list: Product list inventories: Inventories import: Import upload: Upload + csv_templates: CSV Templates + product_list_template: Download Product List template + inventory_template: Download Inventory template + category_values: Available Category Values + product_categories: Product Categories + tax_categories: Tax Categories + shipping_categories: Shipping Categories import: review: Review - proceed: Proceed + import: Import save: Save results: Results save_imported: Save imported products no_valid_entries: No valid entries found none_to_save: There are no entries that can be saved - some_invalid_entries: Imported file contains some invalid entries + some_invalid_entries: Imported file contains invalid entries + fix_before_import: Please fix these errors and try importing the file again save_valid?: Save valid entries for now and discard the others? no_errors: No errors detected! save_all_imported?: Save all imported products? @@ -436,7 +475,8 @@ en_GB: not_found: enterprise could not be found in database no_name: No name blank_supplier: some products have blank supplier name - reset_absent?: Reset absent products? + reset_absent?: Reset absent products + reset_absent_tip: Set stock to zero for all exiting products not present in the file overwrite_all: Overwrite all overwrite_empty: Overwrite if empty default_stock: Set stock level @@ -454,7 +494,7 @@ en_GB: inventory_to_reset: Existing inventory items will have their stock reset to zero line: Line item_line: Item line - save: + save_results: final_results: Import final results products_created: Products created products_updated: Products updated @@ -465,8 +505,9 @@ en_GB: all_saved: "All items saved successfully" some_saved: "items saved successfully" save_errors: Save errors - view_products: View Products - view_inventory: View Inventory + import_again: Upload Another File + view_products: Go To Products Page + view_inventory: Go To Inventory Page variant_overrides: loading_flash: loading_inventory: LOADING INVENTORY @@ -684,7 +725,7 @@ en_GB: email_confirmed: "Email confirmed" email_not_confirmed: "Email not confirmed" actions: - edit_profile: Edit Profile + edit_profile: Settings properties: Properties payment_methods: Payment Methods payment_methods_tip: This enterprise has no payment methods @@ -724,13 +765,14 @@ en_GB: no_enterprises_found: No enterprises found. search_placeholder: Search By Name manage: Manage + manage_link: Settings new_form: owner: Owner owner_tip: The primary user responsible for this enterprise. i_am_producer: I am a Producer contact_name: Contact Name edit: - editing: 'Editing:' + editing: 'Settings:' back_link: Back to enterprises list new: title: New Enterprise @@ -812,6 +854,7 @@ en_GB: products: Products fees: Fees destroy_errors: + orders_present: That order cycle has been selected by a customer and cannot be deleted. To prevent customers from accessing it, please close it instead. schedule_present: That order cycle is linked to a schedule and cannot be deleted. Please unlink or delete the schedule first. bulk_update: no_data: Hm, something went wrong. No order cycle data found. @@ -830,11 +873,6 @@ en_GB: shared: user_guide_link: user_guide: User Guide - invoice_settings: - edit: - title: Invoice Settings - invoice_style2?: Use the alternative invoice model that includes total tax breakdown per rate and tax rate info per item (not yet suitable for countries displaying prices excluding tax) - enable_receipt_printing?: Show options for printing receipts using thermal printers in order dropdown? overview: enterprises_header: ofn_with_tip: Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network. @@ -907,6 +945,7 @@ en_GB: please_select_a_shop: Please select a shop edit_subscription: Edit Subscription pause_subscription: Pause Subscription + unpause_subscription: Unpause Subscription cancel_subscription: Cancel Subscription setup_explanation: just_a_few_more_steps: 'Just a few more steps before you can begin:' @@ -924,14 +963,21 @@ en_GB: reload_this_page: reload this page steps: details: 1. Basic Details + address: 2. Address products: 3. Add Products review: 4. Review & Save + subscription_line_items: + this_is_an_estimate: | + The displayed prices are only an estimate and calculated at the time the subscription is changed. + If you change prices or fees, orders will be updated, but the subscription will still display the old values. details: details: Details invalid_error: Oops! Please fill in all of the required fields... allowed_payment_method_types_tip: Only Cash and Stripe payment methods may be used at the moment credit_card: Credit Card - no_cards_available: No cards available + charges_not_allowed: Charges are not allowed by this customer + no_default_card: Customer has no cards available to charge + card_ok: Customer has a card available to charge loading_flash: loading: LOADING SUBSCRIPTIONS review: @@ -961,22 +1007,6 @@ en_GB: schedules: destroy: associated_subscriptions_error: This schedule cannot be deleted because it has associated subscriptions - stripe_connect_settings: - edit: - title: "Stripe Connect" - settings: "Settings" - stripe_connect_enabled: Enable shops to accept payments using Stripe Connect? - no_api_key_msg: No Stripe account exists for this enterprise. - configuration_explanation_html: For detailed instructions on configuring the Stripe Connect integration, please consult this guide. - status: Status - ok: Ok - instance_secret_key: Instance Secret Key - account_id: Account ID - business_name: Business Name - charges_enabled: Charges Enabled - charges_enabled_warning: "Warning: Charges are not enabled for your account" - auth_fail_error: The API key you provided is invalid - empty_api_key_error_html: No Stripe API key has been provided. To set your API key, please follow these instructions controllers: enterprises: stripe_connect_cancelled: "Connection to Stripe has been cancelled" @@ -1001,6 +1031,33 @@ en_GB: register_call: selling_on_ofn: "Interested in selling through the Open Food Network?" register: "Register here" + footer: + footer_global_headline: "OFN Global" + footer_global_home: "Home" + footer_global_news: "News" + footer_global_about: "About" + footer_global_contact: "Contact" + footer_sites_headline: "OFN Sites" + footer_sites_developer: "Developer" + footer_sites_community: "Community" + footer_sites_userguide: "User Guide" + footer_secure: "Secure and trusted." + footer_secure_text: "Open Food Network uses SSL encryption (2048 bit RSA) everywhere to keep your shopping and payment information private. Our servers do not store your credit card details and payments are processed by PCI-compliant services." + footer_contact_headline: "Keep in touch" + footer_contact_email: "Email us" + footer_nav_headline: "Navigate" + footer_join_headline: "Join us" + footer_join_body: "Create a listing, shop or group directory on the Open Food Network." + footer_join_cta: "Tell me more!" + footer_legal_call: "Read our" + footer_legal_tos: "Terms and conditions" + footer_legal_visit: "Find us on" + footer_legal_text_html: "Open Food Network is a free and open source software platform. Our content is licensed with %{content_license} and our code with %{code_license}." + footer_data_text_with_privacy_policy_html: "We take good care of your data. See our %{privacy_policy} and %{cookies_policy}" + footer_data_text_without_privacy_policy_html: "We take good care of your data. See our %{cookies_policy}" + footer_data_privacy_policy: "privacy policy" + footer_data_cookies_policy: "cookies policy" + footer_skylight_dashboard_html: Performance data is available on %{dashboard}. shop: messages: login: "login" @@ -1036,6 +1093,18 @@ en_GB: ticket_column_item: "Item" ticket_column_unit_price: "Unit Price" ticket_column_total_price: "Total Price" + menu_1_title: "Shops" + menu_1_url: "/shops" + menu_2_title: "Map" + menu_2_url: "/map" + menu_3_title: "Services" + menu_3_url: "https://about.openfoodnetwork.org.uk/services" + menu_5_title: "About" + menu_5_url: "https://about.openfoodnetwork.org.uk" + menu_6_title: "Blog" + menu_6_url: "https://about.openfoodnetwork.org.uk/blog" + menu_7_title: "Support" + menu_7_url: "https://about.openfoodnetwork.org.uk/support" logo: "Logo (640x130)" logo_mobile: "Mobile logo (75x26)" logo_mobile_svg: "Mobile logo (SVG)" @@ -1051,7 +1120,6 @@ en_GB: footer_email: "Email" footer_links_md: "Links" footer_about_url: "About URL" - footer_tos_url: "Terms of Service URL" name: Name first_name: First Name last_name: Last Name @@ -1125,31 +1193,50 @@ en_GB: ie_warning_firefox: Download Firefox ie_warning_ie: Upgrade Internet Explorer ie_warning_other: "Can't upgrade your browser? Try Open Food Network on your smartphone :-)" - footer_global_headline: "OFN Global" - footer_global_home: "Home" - footer_global_news: "News" - footer_global_about: "About" - footer_global_contact: "Contact" - footer_sites_headline: "OFN Sites" - footer_sites_developer: "Developer" - footer_sites_community: "Community" - footer_sites_userguide: "User Guide" - footer_secure: "Secure and trusted." - footer_secure_text: "Open Food Network uses SSL encryption (2048 bit RSA) everywhere to keep your shopping and payment information private. Our servers do not store your credit card details and payments are processed by PCI-compliant services." - footer_contact_headline: "Keep in touch" - footer_contact_email: "Email us" - footer_nav_headline: "Navigate" - footer_join_headline: "Join us" - footer_join_body: "Create a listing, shop or group directory on the Open Food Network." - footer_join_cta: "Tell me more!" - footer_legal_call: "Read our" - footer_legal_tos: "Terms and conditions" - footer_legal_visit: "Find us on" - footer_legal_text_html: "Open Food Network is a free and open source software platform. Our content is licensed with %{content_license} and our code with %{code_license}." - footer_skylight_dashboard_html: Performance data is available on %{dashboard}. + legal: + cookies_policy: + header: "How We Use Cookies" + desc_part_1: "Cookies are very small text files that are stored on your computer when you visit some websites." + desc_part_2: "In OFN we are fully respectful of your privacy. We use only the cookies that are necessary for delivering you the service of selling/buying food online. We don’t sell any of your data. We might in the future propose you to share some of your data to build new commons services that could be useful for the ecosystem (like logistics services for short food systems) but we are not yet there, and we won’t do it without your authorization :-)" + desc_part_3: "We use cookies mainly to remember who you are if you 'log in' to the service, or to be able to remember the items you put in your cart even if you are not logged in. If you keep navigating on the website without clicking on “Accept cookies”, we assume you are giving us consent to store the cookies that are essential for the functioning of the website. Here is the list of cookies we use!" + essential_cookies: "Essential Cookies" + essential_cookies_desc: "The following cookies are strictly necessary for the operation of our website." + essential_cookies_note: "Most cookies only contain a unique identifier, but no other data, so your email address and password for instance are never contained in them and never exposed." + cookie_domain: "Set By:" + cookie_session_desc: "Used to allow the website to remember users between page visits, for example, remember items in your cart." + cookie_consent_desc: "Used to maintain status of user consent to store cookies" + cookie_remember_me_desc: "Used if the user has requested the website to remember him. This cookie is automatically deleted after 12 days. If as a user you want that cookie to be deleted, you only need to logout. If you don’t want that cookie to be installed on your computer you shouldn’t check the “remember me” checkbox when logging in." + cookie_openstreemap_desc: "Used by our friendly open source mapping provider (OpenStreetMap) to ensure that it does not receive too many requests during a given time period, to prevent abuse of their services." + cookie_stripe_desc: "Data collected by our payment processor Stripe for fraud detection https://stripe.com/cookies-policy/legal. Not all shops use Stripe as a payment method but it is a good practice to prevent fraud to apply it to all pages. Stripe probably build a picture of which of our pages usually interact with their API and then flag anything unusual. So setting the Stripe cookie has a broader function than simply the provision of a payment method to a user. Removing it could affect the security of the service itself. You can learn more about Stripe and read its privacy policy at https://stripe.com/privacy." + statistics_cookies: "Statistics Cookies" + statistics_cookies_desc: "The following are not strictly necessary, but help to provide you with the best user experience by allowing us to analyse user behaviour, identify which features you use most, or don’t use, understand user experience issues, etc." + statistics_cookies_analytics_desc_html: "To collect and analyse platform usage data, we use Google Analytics." + statistics_cookies_matomo_desc_html: "To collect and analyse platform usage data, we use Matomo (ex Piwik), an open source analytics tool that is GDPR compliant and protects your privacy." + cookie_analytics_utma_desc: "Used to distinguish users and sessions. The cookie is created when the javascript library executes and no existing __utma cookies exists. The cookie is updated every time data is sent to Google Analytics." + cookie_analytics_utmt_desc: "Used to throttle request rate." + cookie_analytics_utmb_desc: "Used to determine new sessions/visits. The cookie is created when the javascript library executes and no existing __utmb cookies exists. The cookie is updated every time data is sent to Google Analytics." + cookie_analytics_utmc_desc: "Not used in ga.js. Set for interoperability with urchin.js. Historically, this cookie operated in conjunction with the __utmb cookie to determine whether the user was in a new session/visit." + cookie_analytics_utmz_desc: "Stores the traffic source or campaign that explains how the user reached your site. The cookie is created when the javascript library executes and is updated every time data is sent to Google Analytics." + cookie_matomo_basics_desc: "Matomo first party cookies to collect statistics." + cookie_matomo_heatmap_desc: "Matomo Heatmap & Session Recording cookie." + cookie_matomo_ignore_desc: "Cookie used to exclude user from being tracked." + disabling_cookies_header: "Warning on disabling cookies" + disabling_cookies_desc: "As a user you can always allow, block or delete Open Food Network’s or any other website cookies whenever you want to through your browser’s setting control. Each browser has a different operative. Here are the links:" + disabling_cookies_firefox_link: "https://support.mozilla.org/en-US/kb/enable-and-disable-cookies-website-preferences" + disabling_cookies_chrome_link: "https://support.google.com/chrome/answer/95647" + disabling_cookies_ie_link: "https://support.microsoft.com/en-us/help/17442/windows-internet-explorer-delete-manage-cookies" + disabling_cookies_safari_link: "https://www.apple.com/legal/privacy/en-ww/cookies/" + disabling_cookies_note: "But be aware that if you delete or modify the essential cookies used by Open Food Network, the website won’t work, you will not be able to add anything to your cart neither to checkout for instance." + cookies_banner: + cookies_usage: "This site uses cookies in order to make your navigation frictionless and secure, and to help us understand how you use it in order to improve the features we offer." + cookies_definition: "Cookies are very small text files that are stored on your computer when you visit some websites." + cookies_desc: "We use only the cookies that are necessary for delivering you the service of selling/buying food online. We don’t sell any of your data. We use cookies mainly to remember who you are if you ‘log in’ to the service, or to be able to remember the items you put in your cart even if you are not logged in. If you keep navigating on the website without clicking on “Accept cookies”, we assume you are giving us consent to store the cookies that are essential for the functioning of the website." + cookies_policy_link_desc: "If you want to learn more, check our" + cookies_policy_link: "cookies policy" + cookies_accept_button: "Accept Cookies" home_shop: Shop Now brandstory_headline: "Re-imagining Local Food" - brandstory_intro: "Online tools for buying, selling & distributing local food" + brandstory_intro: "A learning community & tools to support local food systems to thrive" brandstory_part1: "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 seriously change the world." brandstory_part2: "Then we need a way to make it real. A way to empower everyone who grows, sells and buys food. A way to tell all the stories, to handle all the logistics. A way to turn transaction into transformation every day." brandstory_part3: "So we build an online marketplace that levels the playing field. It’s transparent, so it creates real relationships. It’s open source, so it’s owned by everyone. It scales to regions and nations, so people start versions across the world." @@ -1236,6 +1323,7 @@ en_GB: email_confirmation_click_link: "Please click the link below to confirm your email and to continue setting up your profile." email_confirmation_link_label: "Confirm this email address »" email_confirmation_help_html: "After confirming your email you can access your administration account for this enterprise. See the %{link} to find out more about %{sitename}'s features and to start using your profile or online store." + email_confirmation_notice_unexpected: "You received this message because you signed up on %{sitename}, or were invited to sign up by someone you probably know. If you don't understand why you are receiving this email, please write to %{contact}." email_social: "Connect with Us:" email_contact: "Email us:" email_signoff: "Cheers," @@ -1266,6 +1354,7 @@ en_GB: email_so_edit_true_html: "You can make changes until orders close on %{orders_close_at}." email_so_edit_false_html: "You can view details of this order at any time." email_so_contact_distributor_html: "If you have any questions you can contact %{distributor} via %{email}." + email_so_contact_distributor_to_change_order_html: "This order was automatically created for you. You can make changes until orders close on %{orders_close_at} by contacting %{distributor} via %{email}." email_so_confirmation_intro_html: "Your order with %{distributor} is now confirmed" email_so_confirmation_explainer_html: "This order was automatically placed for you, and it has now been finalised." email_so_confirmation_details_html: "Here's everything you need to know about your order from %{distributor}:" @@ -1527,6 +1616,7 @@ en_GB: error_number: "must be number" error_email: "must be email address" error_not_found_in_database: "%{name} not found in database" + error_not_primary_producer: "%{name} is not enabled as a producer" error_no_permission_for_enterprise: "\"%{name}\": you do not have permission to manage products for this enterprise" item_handling_fees: "Item Handling Fees (included in item totals)" january: "January" @@ -1558,6 +1648,17 @@ en_GB: reset_password: "Reset password" who_is_managing_enterprise: "Who is responsible for managing %{enterprise}?" update_and_recalculate_fees: "Update And Recalculate Fees" + registration: + steps: + type: + headline: "Last step to add %{enterprise}!" + question: "Are you a producer?" + yes_producer: "Yes, I'm a producer" + no_producer: "No, I'm not a producer" + producer_field_error: "Please choose one. Are you are producer?" + yes_producer_help: "Producers make tasty things to eat and/or drink. You're a producer if you grow, rear, brew, bake, ferment ... etc." + 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" enterprise: registration: modal: @@ -1596,13 +1697,6 @@ en_GB: phone_field_placeholder: 'eg. 07123123123' type: title: 'Type' - headline: "Last step to add %{enterprise}!" - question: "Are you a producer?" - yes_producer: "Yes, I'm a producer" - no_producer: "No, I'm not a producer" - producer_field_error: "Please choose one. Are you are producer?" - yes_producer_help: "Producers make tasty things to eat and/or drink. You're a producer if you grow, rear, brew, bake, ferment ... etc." - 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." about: title: 'About' images: @@ -1641,6 +1735,7 @@ en_GB: enterprise_about_headline: "Nice one!" enterprise_about_message: "Now let's flesh out the details about" enterprise_success: "Success! %{enterprise} added to the Open Food Network " + enterprise_registration_exit_message: "If you exit this wizard at any stage, you can continue to create your profile by going to the admin interface." enterprise_description: "Short Description" enterprise_description_placeholder: "A short sentence describing your enterprise" enterprise_long_desc: "Long Description" @@ -1690,7 +1785,6 @@ en_GB: registration_type_error: "Please choose one. Are you are producer?" registration_type_producer_help: "Producers make things to eat and/or drink. You're a producer if you might grow it, raise it, brew it, bake it or ferment 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" registration_images_headline: "Thanks!" registration_images_description: "Let's upload some pictures so your profile looks great! :)" registration_detail_headline: "Let's get started" @@ -1749,7 +1843,7 @@ en_GB: 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: "Enterprise Permissions" admin_entreprise_relationships_everything: "Everything" admin_entreprise_relationships_permits: "permits" admin_entreprise_relationships_seach_placeholder: "Search" @@ -1873,11 +1967,8 @@ en_GB: edit_profile_details_etc: "Change your profile description, images, etc." order_cycle: "Order Cycle" order_cycles: "Order Cycles" - enterprises: "Enterprises" - enterprise_relationships: "Enterprise relationships" + enterprise_relationships: "Enterprise permissions" remove_tax: "Remove tax" - enterprise_terms_of_service: "Enterprise Terms of Service" - enterprises_require_tos: "Enterprises must accept Terms of Service" enterprise_tos_link: "Enterprise Terms of Service link" enterprise_tos_message: "We want to work with people that share our aims and values. As such we ask new enterprises to agree to our " enterprise_tos_link_text: "Terms of Service." @@ -2277,6 +2368,11 @@ en_GB: resolve: Resolve new_tag_rule_dialog: select_rule_type: "Select a rule type:" + resend_user_email_confirmation: + resend: "Resend" + sending: "Resend..." + done: "Resend done ✓" + failed: "Resend failed ✗" out_of_stock: reduced_stock_available: Reduced stock available out_of_stock_text: > @@ -2380,7 +2476,8 @@ en_GB: payments: source_forms: stripe: - no_payment_via_admin_backend: Creating Stripe-based payments from the admin backend is not possible at this time + error_saving_payment: Error saving payment + submitting_payment: Submitting payment... products: new: title: 'New Product' @@ -2421,12 +2518,21 @@ en_GB: bulk_coop_allocation: 'Bulk Co-op - Allocation' bulk_coop_packing_sheets: 'Bulk Co-op - Packing Sheets' bulk_coop_customer_payments: 'Bulk Co-op - Customer Payments' - shared: - configuration_menu: - stripe_connect: Stripe Connect + users: + email_confirmation: + confirmation_pending: "Email confirmation is pending. We've sent a confirmation email to %{address}." variants: autocomplete: producer_name: Producer + general_settings: + edit: + legal_settings: "Legal Settings" + cookies_consent_banner_toggle: "Display cookies consent banner" + privacy_policy_url: "Privacy Policy URL" + enterprises_require_tos: "Enterprises must accept Terms of Service" + cookies_policy_matomo_section: "Display Matomo section on cookies policy page" + cookies_policy_ga_section: "Display Google Analytics section on cookies policy page" + footer_tos_url: "Terms of Service URL" checkout: payment: stripe: @@ -2494,6 +2600,8 @@ en_GB: issue_text: | If the above URL does not work try copying and pasting it into your browser. If you continue to have problems please feel free to contact us. + confirmation_instructions: + subject: Please confirm your OFN account weight: Weight (per kg) zipcode: Postcode users: @@ -2505,6 +2613,7 @@ en_GB: cards: Credit Cards transactions: Transactions settings: Account Settings + unconfirmed_email: "Pending email confirmation for: %{unconfirmed_email}. Your email address will be updated once the new email is confirmed." orders: open_orders: Open Orders past_orders: Past Orders @@ -2528,5 +2637,12 @@ en_GB: total: Total paid?: Paid? view: View + saved_cards: + default?: Default? + delete?: Delete? + cards: + authorised_shops: Authorised Shops + authorised_shops_popover: This is the list of shops which are permitted to charge your default credit card for any subscriptions (ie. repeating orders) you may have. Your card details will be kept secure and will not be shared with shop owners. You will always be notified when you are charged. + saved_cards_popover: This is the list of cards you have opted to save for later use. Your 'default' will be selected automatically when you checkout an order, and can be charged by any shops you have allowed to do so (see right). localized_number: invalid_format: has an invalid format. Please enter a number. diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 0da03df1c6..cd3786c5b2 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -18,8 +18,6 @@ en_US: attributes: email: taken: "There's already an account registered for this email. Please login to reset your password." - spree/order: - no_card: There are no valid credit cards available order_cycle: attributes: orders_close_at: @@ -43,9 +41,6 @@ en_US: invalid_type: "must be a Cash or Stripe method" shipping_method: not_available_to_shop: "is not available to %{shop}" - credit_card: - not_available: "is not available" - blank: "is required" devise: confirmations: send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." @@ -156,6 +151,7 @@ en_US: distributors: Distributors distribution: Distribution bulk_order_management: Bulk Order Management + enterprises: Enterprises enterprise_groups: Groups reports: Reports variant_overrides: Inventory @@ -233,6 +229,8 @@ en_US: form_invalid: "Form contains missing or invalid fields" clear_filters: Clear Filters clear: Clear + cancel: Cancel + back: Back show_more: Show more show_n_more: Show %{num} more choose: "Choose..." @@ -303,6 +301,35 @@ en_US: included_tax_tip: "The total tax included in the example monthly bill, given the settings and the turnover provided." total_monthly_bill_incl_tax: "Total Monthly Bill (Incl. Tax)" total_monthly_bill_incl_tax_tip: "The example total monthly bill with tax included, given the settings and the turnover provided." + cache_settings: + show: + title: Caching + distributor: Distributor + order_cycle: Order Cycle + status: Status + diff: Diff + error: Error + invoice_settings: + edit: + title: Invoice Settings + invoice_style2?: Use the alternative invoice model that includes total tax breakdown per rate and tax rate info per item (not yet suitable for countries displaying prices excluding tax) + enable_receipt_printing?: Show options for printing receipts using thermal printers in order dropdown? + stripe_connect_settings: + edit: + title: "Stripe Connect" + settings: "Settings" + stripe_connect_enabled: Enable shops to accept payments using Stripe Connect? + no_api_key_msg: No Stripe account exists for this enterprise. + configuration_explanation_html: For detailed instructions on configuring the Stripe Connect integration, please consult this guide. + status: Status + ok: Ok + instance_secret_key: Instance Secret Key + account_id: Account ID + business_name: Business Name + charges_enabled: Charges Enabled + charges_enabled_warning: "Warning: Charges are not enabled for your account" + auth_fail_error: The API key you provided is invalid + empty_api_key_error_html: No Stripe API key has been provided. To set your API key, please follow these instructions customers: index: add_customer: "Add Customer" @@ -327,14 +354,6 @@ en_US: search_by_email: "Search by email/code..." destroy: has_associated_orders: 'Delete failed: customer has associated orders with his shop' - cache_settings: - show: - title: Caching - distributor: Distributor - order_cycle: Order Cycle - status: Status - diff: Diff - error: Error contents: edit: title: Content @@ -386,6 +405,14 @@ en_US: product_distributions: "Product Distributions" group_buy_options: "Group Buy Options" back_to_products_list: "Back to products list" + product_import: + file_not_found: File not found or could not be opened + no_data: No data found in spreadsheet + model: + no_file: "error: no file uploaded" + could_not_process: "could not process file: invalid filetype" + blank: can't be blank + none_saved: did not save any products successfully variant_overrides: loading_flash: loading_inventory: LOADING INVENTORY @@ -602,7 +629,7 @@ en_US: email_confirmed: "Email confirmed" email_not_confirmed: "Email not confirmed" actions: - edit_profile: Edit Profile + edit_profile: Settings properties: Properties payment_methods: Payment Methods payment_methods_tip: This enterprise has no payment methods @@ -642,13 +669,13 @@ en_US: no_enterprises_found: No enterprises found. search_placeholder: Search By Name manage: Manage + manage_link: Settings new_form: owner: Owner owner_tip: The primary user responsible for this enterprise. i_am_producer: I am a Producer contact_name: Contact Name edit: - editing: 'Editing:' back_link: Back to enterprises list new: title: New Enterprise @@ -657,7 +684,6 @@ en_US: welcome_title: Welcome to the Open Food Network! welcome_text: You have successfully created a next_step: Next step - choose_starting_point: 'Choose your starting point:' invite_manager: user_already_exists: "User already exists" error: "Something went wrong" @@ -717,7 +743,7 @@ en_US: name: Name orders_open: Orders open at coordinator: Coordinator - order_closes: Orders close + orders_close: Orders close row: suppliers: suppliers distributors: distributors @@ -734,6 +760,8 @@ en_US: schedule_present: That order cycle is linked to a schedule and cannot be deleted. Please unlink or delete the schedule first. bulk_update: no_data: Hm, something went wrong. No order cycle data found. + date_warning: + cancel: Cancel producer_properties: index: title: Producer Properties @@ -745,11 +773,6 @@ en_US: shared: user_guide_link: user_guide: User Guide - invoice_settings: - edit: - title: Invoice Settings - invoice_style2?: Use the alternative invoice model that includes total tax breakdown per rate and tax rate info per item (not yet suitable for countries displaying prices excluding tax) - enable_receipt_printing?: Show options for printing receipts using thermal printers in order dropdown? overview: enterprises_header: ofn_with_tip: Enterprises are Producers and/or Hubs and are the basic unit of organization within the Open Food Network. @@ -848,7 +871,6 @@ en_US: invalid_error: Oops! Please fill in all of the required fields... allowed_payment_method_types_tip: Only Cash and Stripe payment methods may be used at the moment credit_card: Credit Card - no_cards_available: No cards available loading_flash: loading: LOADING SUBSCRIPTIONS review: @@ -878,22 +900,6 @@ en_US: schedules: destroy: associated_subscriptions_error: This schedule cannot be deleted because it has associated subscriptions - stripe_connect_settings: - edit: - title: "Stripe Connect" - settings: "Settings" - stripe_connect_enabled: Enable shops to accept payments using Stripe Connect? - no_api_key_msg: No Stripe account exists for this enterprise. - configuration_explanation_html: For detailed instructions on configuring the Stripe Connect integration, please consult this guide. - status: Status - ok: Ok - instance_secret_key: Instance Secret Key - account_id: Account ID - business_name: Business Name - charges_enabled: Charges Enabled - charges_enabled_warning: "Warning: Charges are not enabled for your account" - auth_fail_error: The API key you provided is invalid - empty_api_key_error_html: No Stripe API key has been provided. To set your API key, please follow these instructions controllers: enterprises: stripe_connect_cancelled: "Connection to Stripe has been cancelled" @@ -918,6 +924,28 @@ en_US: register_call: selling_on_ofn: "Interested in selling through the Open Food Network?" register: "Register here" + footer: + footer_global_headline: "OFN Global" + footer_global_home: "Home" + footer_global_news: "News" + footer_global_about: "About" + footer_global_contact: "Contact" + footer_sites_headline: "OFN Sites" + footer_sites_developer: "Developer" + footer_sites_community: "Community" + footer_sites_userguide: "User Guide" + footer_secure: "Secure and trusted." + footer_secure_text: "Open Food Network uses SSL encryption (2048 bit RSA) everywhere to keep your shopping and payment information private. Our servers do not store your credit card details and payments are processed by PCI-compliant services." + footer_contact_headline: "Keep in touch" + footer_contact_email: "Email us" + footer_nav_headline: "Navigate" + footer_join_headline: "Join us" + footer_join_body: "Create a listing, shop or group directory on the Open Food Network." + footer_join_cta: "Tell me more!" + footer_legal_call: "Read our" + footer_legal_tos: "Terms and conditions" + footer_legal_visit: "Find us on" + footer_legal_text_html: "Open Food Network is a free and open source software platform. Our content is licensed with %{content_license} and our code with %{code_license}." shop: messages: login: "login" @@ -952,6 +980,13 @@ en_US: ticket_column_item: "Item" ticket_column_unit_price: "Unit Price" ticket_column_total_price: "Total Price" + menu_1_title: "Stores" + menu_2_title: "Map" + menu_3_title: "Producers" + menu_4_title: "Groups" + menu_5_title: "About" + menu_6_title: "Connect" + menu_7_title: "Learn" logo: "Logo (640x130)" logo_mobile: "Mobile logo (75x26)" logo_mobile_svg: "Mobile logo (SVG)" @@ -967,7 +1002,6 @@ en_US: footer_email: "Email" footer_links_md: "Links" footer_about_url: "About URL" - footer_tos_url: "Terms of Service URL" name: Name first_name: First name last_name: Last name @@ -1041,27 +1075,6 @@ en_US: ie_warning_firefox: Download Firefox ie_warning_ie: Upgrade Internet Explorer ie_warning_other: "Can't upgrade your browser? Try Open Food Network on your smartphone :-)" - footer_global_headline: "OFN Global" - footer_global_home: "Home" - footer_global_news: "News" - footer_global_about: "About" - footer_global_contact: "Contact" - footer_sites_headline: "OFN Sites" - footer_sites_developer: "Developer" - footer_sites_community: "Community" - footer_sites_userguide: "User Guide" - footer_secure: "Secure and trusted." - footer_secure_text: "Open Food Network uses SSL encryption (2048 bit RSA) everywhere to keep your shopping and payment information private. Our servers do not store your credit card details and payments are processed by PCI-compliant services." - footer_contact_headline: "Keep in touch" - footer_contact_email: "Email us" - footer_nav_headline: "Navigate" - footer_join_headline: "Join us" - footer_join_body: "Create a listing, shop or group directory on the Open Food Network." - footer_join_cta: "Tell me more!" - footer_legal_call: "Read our" - footer_legal_tos: "Terms and conditions" - footer_legal_visit: "Find us on" - footer_legal_text_html: "Open Food Network is a free and open source software platform. Our content is licensed with %{content_license} and our code with %{code_license}." home_shop: Shop Now brandstory_headline: "Food, unincorporated." brandstory_intro: "Sometimes the best way to fix the system is to start a new one…" @@ -1466,6 +1479,17 @@ en_US: reset_password: "Reset password" who_is_managing_enterprise: "Who is responsible for managing %{enterprise}?" update_and_recalculate_fees: "Update And Recalculate Fees" + registration: + steps: + type: + headline: "Last step to add %{enterprise}!" + question: "Are you a producer?" + yes_producer: "Yes, I'm a producer" + no_producer: "No, I'm not a producer" + producer_field_error: "Please choose one. Are you are producer?" + yes_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 mold it." + 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" enterprise: registration: modal: @@ -1504,13 +1528,6 @@ en_US: phone_field_placeholder: 'eg. (123)456-7891' type: title: 'Type' - headline: "Last step to add %{enterprise}!" - question: "Are you a producer?" - yes_producer: "Yes, I'm a producer" - no_producer: "No, I'm not a producer" - producer_field_error: "Please choose one. Are you are producer?" - yes_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 mold it." - 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." about: title: 'About' images: @@ -1598,7 +1615,6 @@ en_US: 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" 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" @@ -1657,7 +1673,6 @@ en_US: 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" @@ -1740,12 +1755,7 @@ en_US: 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_tabs_hubs: "HUBS" - spree_admin_enterprises_tabs_producers: "PRODUCERS" spree_admin_enterprises_producers_manage_products: "MANAGE PRODUCTS" spree_admin_enterprises_any_active_products_text: "You don't have any active products." spree_admin_enterprises_create_new_product: "CREATE A NEW PRODUCT" @@ -1786,11 +1796,7 @@ en_US: edit_profile_details_etc: "Change your profile description, images, etc." order_cycle: "Order Cycle" order_cycles: "Order Cycles" - enterprises: "Enterprises" - enterprise_relationships: "Enterprise relationships" remove_tax: "Remove tax" - enterprise_terms_of_service: "Enterprise Terms of Service" - enterprises_require_tos: "Enterprises must accept Terms of Service" enterprise_tos_link: "Enterprise Terms of Service link" enterprise_tos_message: "We want to work with people that share our aims and values. As such we ask new enterprises to agree to our " enterprise_tos_link_text: "Terms of Service." @@ -2010,7 +2016,6 @@ en_US: content_configuration_pricing_table: "(TODO: Pricing table)" content_configuration_case_studies: "(TODO: Case studies)" content_configuration_detail: "(TODO: Detail)" - enterprise_name_error: "has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at %{email}." enterprise_owner_error: "^%{email} is not permitted to own any more enterprises (limit is %{enterprise_limit})." enterprise_role_uniqueness_error: "^That role is already present." inventory_item_visibility_error: must be true or false @@ -2188,6 +2193,8 @@ en_US: resolve: Resolve new_tag_rule_dialog: select_rule_type: "Select a rule type:" + resend_user_email_confirmation: + resend: "Resend" out_of_stock: reduced_stock_available: Reduced stock available out_of_stock_text: > @@ -2288,10 +2295,6 @@ en_US: account_id: Account ID business_name: Business Name charges_enabled: Charges Enabled - payments: - source_forms: - stripe: - no_payment_via_admin_backend: Creating Stripe-based payments from the admin backend is not possible at this time products: new: title: 'New Product' @@ -2329,12 +2332,13 @@ en_US: bulk_coop_allocation: 'Bulk Co-op - Allocation' bulk_coop_packing_sheets: 'Bulk Co-op - Packing Sheets' bulk_coop_customer_payments: 'Bulk Co-op - Customer Payments' - shared: - configuration_menu: - stripe_connect: Stripe Connect variants: autocomplete: producer_name: Producer + general_settings: + edit: + enterprises_require_tos: "Enterprises must accept Terms of Service" + footer_tos_url: "Terms of Service URL" checkout: payment: stripe: diff --git a/config/locales/es.yml b/config/locales/es.yml index e990f79e55..f4c79ff1a9 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -19,7 +19,7 @@ es: email: taken: "Ya existe una cuenta con este email. Inicie sesión o restablezca tu contraseña." spree/order: - no_card: No hay tarjetas de crédito válidas disponibles + no_card: No hay tarjetas de crédito autorizadas disponibles para cargar order_cycle: attributes: orders_close_at: @@ -41,11 +41,10 @@ es: payment_method: not_available_to_shop: "no está disponible para %{shop}" invalid_type: "El método debe ser Cash o Stripe" + charges_not_allowed: "^ Los cargos de la tarjeta de crédito no estan permitidos para esta consumidora" + no_default_card: "^ Ninguna tarjeta predeterminada disponible para esta consumidora" shipping_method: not_available_to_shop: "no está disponible para %{shop}" - credit_card: - not_available: "no está disponible" - blank: "Requerido" devise: confirmations: send_instructions: "Recibirás un correo electrónico con instrucciones sobre cómo confirmar su cuenta en unos minutos." @@ -61,6 +60,10 @@ es: Correo o contraseña inválidos. ¿Has sido invitada? Tal vez necesites crear una cuenta o recuperar tu contraseña. unconfirmed: "Debes confirmar tu cuenta antes de continuar." + already_registered: "Esta dirección de correo electrónico ya está registrada. Inicie sesión para continuar, o vuelva atrás y use otra dirección de correo electrónico." + user_passwords: + spree_user: + updated_not_active: "Su contraseña ha sido restablecida, pero su correo electrónico aún no ha sido confirmado." enterprise_mailer: confirmation_instructions: subject: "Confirma la dirección de correo electrónico de %{enterprise}" @@ -156,6 +159,7 @@ es: distributors: Distribuidores distribution: Distribución bulk_order_management: Gestión de pedidos en bloque + enterprises: Organizaciones enterprise_groups: Redes reports: Informes variant_overrides: Inventario @@ -233,6 +237,9 @@ es: form_invalid: "El formulario contiene campos vacíos o inválidos" clear_filters: Limpiar filtros clear: Limpiar + save: Guardar + cancel: Cancelar + back: Atrás show_more: Mostrar más show_n_more: Mostrar %{num} más choose: "Escoger..." @@ -303,6 +310,42 @@ es: included_tax_tip: "El impuesto total incluido en la factura mensual de ejemplo, dada la configuración y la facturación proporcionada." total_monthly_bill_incl_tax: "Factura mensual total (incl. impuesto)" total_monthly_bill_incl_tax_tip: "El ejemplo de factura mensual total con impuestos incluidos, dada la configuración y la facturación proporcionada." + cache_settings: + show: + title: Almacenando + distributor: Distribuidor + order_cycle: Ciclo de Pedido + status: Estado + diff: Diff + error: Error + invoice_settings: + edit: + title: Configuración de Factura + invoice_style2?: Utiliza el modelo de factura alternativo que incluye el desglose fiscal total por tipo de interés y tasa de impuestos por artículo (todavía no es adecuado para países que muestran los precios sin impuestos) + enable_receipt_printing?: ¿Mostrar opciones para imprimir recibos usando impresoras térmicas en el desplegable del pedido? + stripe_connect_settings: + edit: + title: "Stripe Connect" + settings: "Configuración" + stripe_connect_enabled: ¿Permitir a las tiendas aceptar pagos mediante Stripe Connect? + no_api_key_msg: No existe una cuenta Stripe para esta organización. + configuration_explanation_html: Para obtener instrucciones detalladas sobre cómo configurar la integración con Stripe Connect, consulte esta guía . + status: Estado + ok: Ok + instance_secret_key: Clave secreta de instancia + account_id: Account ID + business_name: Nombre de la Organización + charges_enabled: Cargos habilitados + charges_enabled_warning: "Advertencia: los cargos no están habilitados para su cuenta" + auth_fail_error: La clave API que proporcionó no es válida. + empty_api_key_error_html: No se ha proporcionado ninguna clave API Stripe. Para configurar su clave API, siga estas instrucciones + matomo_settings: + edit: + title: "Configuración de Matomo" + matomo_url: "URL de Matomo" + matomo_site_id: "ID de sitio de Matomo" + info_html: "Matomo es un analizador web y móvil. Puede alojar Matomo en sus servidores o utilizar un servicio alojado en la nube. Consulte matomo.org para obtener más información." + config_instructions_html: "Aquí puede configurar la integración de OFN Matomo. La siguiente URL de Matomo debe apuntar a la instancia de Matomo a la que se enviará la información de seguimiento del usuario; si se deja vacío, el seguimiento del usuario Matomo se desactivará. El campo ID del sitio no es obligatorio, pero es útil si está rastreando más de un sitio web en una sola instancia de Matomo; se puede encontrar en la consola de la instancia de Matomo." customers: index: add_customer: "Añadir Consumidor" @@ -325,16 +368,9 @@ es: update_address: 'Actualizar Dirección' confirm_delete: '¿Confirmas que quieres borrar?' search_by_email: "Buscar por email/código" + guest_label: 'Hacer pedido como invitado' destroy: has_associated_orders: 'Se ha producido un error en la eliminación: la consumidora tiene pedidos asociados en su tienda.' - cache_settings: - show: - title: Almacenando - distributor: Distribuidor - order_cycle: Ciclo de Pedido - status: Estado - diff: Diff - error: Error contents: edit: title: Contenido @@ -343,6 +379,7 @@ es: producer_signup_page: Página de registro del productor hub_signup_page: Página de registro del Grupo group_signup_page: Página de registro de grupo + main_links: Enlaces al menú principal footer_and_external_links: Pie de página y enlaces externos your_content: Tu contenido enterprise_fees: @@ -372,6 +409,7 @@ es: inherits_properties?: ¿Hereda propiedades? available_on: Disponible en av_on: "Av. En" + import_date: Importado upload_an_image: Subir una imagen product_search_keywords: Palabras clave de búsqueda de productos product_search_tip: Escriba palabras para ayudar a buscar sus productos en las tiendas. Use espacio para separar cada palabra clave. @@ -386,6 +424,55 @@ es: product_distributions: "Distribuciones de productos" group_buy_options: "Opciones de compra grupales" back_to_products_list: "Volver a la lista de productos" + product_import: + title: Importación de productos + file_not_found: Archivo no encontrado o no se pudo abrir + no_data: No se encontraron datos en la hoja de cálculo + confirm_reset: "Esto establecerá el nivel de stock en cero en todos los productos para esta\n organizacion que no están presentes en el archivo cargado" + model: + no_file: "Error: no se ha subido ningún archivo" + could_not_process: "No se pudo procesar el archivo: tipo de archivo inválido" + incorrect_value: valor incorrecto + conditional_blank: no puede estar en blanco si unit_type está en blanco + no_product: no coincide con ningún producto en la base de datos + not_found: no encontrado en la base de datos + blank: no puede estar vacío + products_no_permission: no tienes permiso para administrar productos para esta organización + inventory_no_permission: no tienes permiso para crear inventario para esta productora + none_saved: No se guardó ningún producto con éxito + line: Línea + index: + select_file: Selecciona una hoja de cálculo para subir + spreadsheet: Hoja de cálculo + choose_import_type: Seleccionar tipo de importación + import_into: Tipo de importación + product_list: Lista de productos + inventories: Inventarios + import: Importar + upload: Subir + csv_templates: Plantillas CSV + product_list_template: Descargar la plantilla de lista de productos + inventory_template: Descargar plantilla de inventario + category_values: Valores de categoría disponibles + product_categories: Categorías de Producto + tax_categories: Categorías de impuestos + shipping_categories: Categorías de envío + import: + review: Revisión + import: Importar + save: Guardar + results: Resultados + save_imported: Guardar productos importados + no_valid_entries: No se encontraron entradas válidas + none_to_save: No hay entradas que se puedan guardar + some_invalid_entries: El archivo importado contiene entradas no válidas + fix_before_import: Corrija estos errores e intente importar el archivo nuevamente + save_valid?: ¿Guardar entradas válidas por ahora y descartar las demás? + no_errors: ¡No se detectaron errores! + save_all_imported?: Guardar todos los productos importados? + options_and_defaults: Opciones de importación y valores predeterminados + no_permission: no tienes permiso para administrar esta organización + line: Línea variant_overrides: loading_flash: loading_inventory: CARGANDO INVENTARIO @@ -396,6 +483,7 @@ es: inherit?: ¿Heredado? add: Añadir hide: Ocultar + import_date: Importado select_a_shop: Selecciona una Tienda review_now: Revisar ahora new_products_alert_message: Hay %{new_product_count} nuevos productos disponibles para añadir a tu inventario. @@ -604,7 +692,7 @@ es: email_confirmed: "Correo electrónico confirmado" email_not_confirmed: "Correo electrónico no confirmado" actions: - edit_profile: Editar Perfil + edit_profile: Configuración properties: Propiedades payment_methods: Métodos de Pago payment_methods_tip: Esta organización no tiene métodos de pago @@ -644,13 +732,13 @@ es: no_enterprises_found: No se encuentran organizaciones. search_placeholder: Buscar por Nombre manage: Gestionar + manage_link: Configuración new_form: owner: Propietaria owner_tip: La principal usaría responsable para esta organización. i_am_producer: Soy una Productora contact_name: Nombre de Contacto edit: - editing: 'Editando:' back_link: Volver a la lista de organizaciones new: title: Nueva Organización @@ -659,7 +747,6 @@ es: welcome_title: Bienvenida a Open Food Network! welcome_text: Has creado correctamente un next_step: Siguiente paso - choose_starting_point: 'Elige tu punto de partida:' invite_manager: user_already_exists: "El usuario ya existe" error: "Algo salió mal" @@ -719,7 +806,7 @@ es: name: Nombre orders_open: Pedidos abiertos a coordinator: Coordinadora - order_closes: Cierre de Pedidos + orders_close: Cierre de Pedidos row: suppliers: proveedoras distributors: distribuidoras @@ -736,6 +823,8 @@ es: schedule_present: Ese ciclo de pedido está vinculado a un horario y no puede ser eliminado. Desvincula o elimina el calendario primero. bulk_update: no_data: Hm, algo salió mal. No se encontraron datos de ciclo de pedido. + date_warning: + cancel: Cancelar producer_properties: index: title: Propiedades de la Productora @@ -747,11 +836,6 @@ es: shared: user_guide_link: user_guide: Manual de Usuario - invoice_settings: - edit: - title: Configuración de Factura - invoice_style2?: Utiliza el modelo de factura alternativo que incluye el desglose fiscal total por tipo de interés y tasa de impuestos por artículo (todavía no es adecuado para países que muestran los precios sin impuestos) - enable_receipt_printing?: ¿Mostrar opciones para imprimir recibos usando impresoras térmicas en el desplegable del pedido? overview: enterprises_header: ofn_with_tip: Las Organizaciones son Productoras y/o Grupos y son la unidad básica de organización dentro de la Open Food Network. @@ -850,7 +934,6 @@ es: invalid_error: Ups! Por favor complete todos los campos requeridos ... allowed_payment_method_types_tip: Solo se pueden usar métodos de pago en efectivo y Stripe en este momento credit_card: Tarjeta de crédito - no_cards_available: No hay tarjetas disponibles loading_flash: loading: CARGANDO SUSCRIPCIONES review: @@ -880,22 +963,6 @@ es: schedules: destroy: associated_subscriptions_error: Este horario no se puede eliminar porque tiene suscripciones asociadas - stripe_connect_settings: - edit: - title: "Stripe Connect" - settings: "Configuración" - stripe_connect_enabled: ¿Permitir a las tiendas aceptar pagos mediante Stripe Connect? - no_api_key_msg: No existe una cuenta Stripe para esta organización. - configuration_explanation_html: Para obtener instrucciones detalladas sobre cómo configurar la integración con Stripe Connect, consulte esta guía . - status: Estado - ok: Ok - instance_secret_key: Clave secreta de instancia - account_id: Account ID - business_name: Nombre de la Organización - charges_enabled: Cargos habilitados - charges_enabled_warning: "Advertencia: los cargos no están habilitados para su cuenta" - auth_fail_error: La clave API que proporcionó no es válida. - empty_api_key_error_html: No se ha proporcionado ninguna clave API Stripe. Para configurar su clave API, siga estas instrucciones controllers: enterprises: stripe_connect_cancelled: "Se ha cancelado la conexión a Stripe" @@ -920,6 +987,28 @@ es: register_call: selling_on_ofn: "¿Estás interesada en entrar en Open Food Network?" register: "Regístrate aquí" + footer: + footer_global_headline: "OFN Global" + footer_global_home: "Inicio" + footer_global_news: "Noticas" + footer_global_about: "Acerca de" + footer_global_contact: "Contacto" + footer_sites_headline: "Sitios OFN" + footer_sites_developer: "Desarrollador" + footer_sites_community: "Comunidad" + footer_sites_userguide: "Manual de Usuario" + footer_secure: "Seguro y de confianza." + footer_secure_text: "Open Food Network usa cifrado SSL (RSA de 2048 bit) en toda su plataforma para mantener privada la información de compras y pagos. Nuestros servidores no almacenan los detalles de tarjetas de créditos y los pagos son procesados por servicios que cumplen con PCI." + footer_contact_headline: "Mantenerse en contacto" + footer_contact_email: "Envíenos un correo electrónico" + footer_nav_headline: "Navegar" + footer_join_headline: "Unirse" + footer_join_body: "Crea un listado, una tienda o un directorio en Open Food Network." + footer_join_cta: "Cuentame más!" + footer_legal_call: "Leer nuestros" + footer_legal_tos: "Terminos y condiciones" + footer_legal_visit: "Encuéntrenos en" + footer_legal_text_html: "Open Food Network es una plataforma libre y de código abierto. Nuestro contenido tiene una licencia %{content_license} y nuestro código %{code_license}." shop: messages: login: "login" @@ -954,6 +1043,14 @@ es: ticket_column_item: "Artículo" ticket_column_unit_price: "Precio por unidad" ticket_column_total_price: "Precio total" + menu_1_title: "Tiendas" + menu_2_title: "Mapa" + menu_3_title: "Productoras" + menu_4_title: "Redes" + menu_5_title: "Acerca de" + menu_5_url: "http://katuma.org/" + menu_6_title: "Conectar" + menu_7_title: "Aprender" logo: "Logo (640x130)" logo_mobile: "Logo para móvil (75x26)" logo_mobile_svg: "Logo para móvil (SVG)" @@ -969,7 +1066,6 @@ es: footer_email: "Correo electrónico" footer_links_md: "Enlaces" footer_about_url: "URL acerca de" - footer_tos_url: "URL de términos y servicios" name: Nombre first_name: Nombre last_name: Apellido @@ -1043,27 +1139,6 @@ es: ie_warning_firefox: Descargar Firefox ie_warning_ie: Actualizar Internet Explorer ie_warning_other: "¿No puede actualizar su navegador? Pruebe Open Food Network en su teléfono :-)" - footer_global_headline: "OFN Global" - footer_global_home: "Inicio" - footer_global_news: "Noticas" - footer_global_about: "Acerca de" - footer_global_contact: "Contacto" - footer_sites_headline: "Sitios OFN" - footer_sites_developer: "Desarrollador" - footer_sites_community: "Comunidad" - footer_sites_userguide: "Guía de usuario" - footer_secure: "Seguro y de confianza." - footer_secure_text: "Open Food Network usa cifrado SSL (RSA de 2048 bit) en toda su plataforma para mantener privada la información de compras y pagos. Nuestros servidores no almacenan los detalles de tarjetas de créditos y los pagos son procesados por servicios que cumplen con PCI." - footer_contact_headline: "Mantenerse en contacto" - footer_contact_email: "Envíenos un correo" - footer_nav_headline: "Navegar" - footer_join_headline: "Unirse" - footer_join_body: "Crea un listado, una tienda o un directorio en Open Food Network." - footer_join_cta: "Cuentame más!" - footer_legal_call: "Leer nuestros" - footer_legal_tos: "Terminos y condiciones" - footer_legal_visit: "Encuéntrenos en" - footer_legal_text_html: "Open Food Network es una plataforma libre y de código abierto. Nuestro contenido tiene una licencia %{content_license} y nuestro código %{code_license}." home_shop: Comprar ahora brandstory_headline: "Consume con valores." brandstory_intro: "A veces la mejor forma de arreglar el sistema es empezar uno nuevo…" @@ -1475,6 +1550,17 @@ es: reset_password: "Restaurar contraseña" who_is_managing_enterprise: "¿Quién es responsable de administrar %{enterprise}?" update_and_recalculate_fees: "Actualizar y recalcular tarifas" + registration: + steps: + type: + headline: "Último paso para añadir %{enterprise}!" + question: "¿Eres una productora?" + yes_producer: "Si soy una productora" + no_producer: "No, no soy una productora" + producer_field_error: "Por favor elige uno. ¿Eres una productora?" + yes_producer_help: "Las productoras hacen cosas deliciosas para comer y/o beber. Eres una productora si lo cultivas, lo haces crecer, lo preparas, lo horneas, lo fermentas, lo ordeñas, ..." + no_producer_help: "Si no eres una productora, probablemente conozcas a alguien que venda o distribuya comida. También podrías convertirte en un grupo de consumo u otro tipo de organización." + create_profile: "Crear Perfil" enterprise: registration: modal: @@ -1513,13 +1599,6 @@ es: phone_field_placeholder: 'p.ej. 651 01 20 54' type: title: 'Tipo' - headline: "Último paso para añadir %{enterprise}!" - question: "¿Eres una productora?" - yes_producer: "Si soy una productora" - no_producer: "No, no soy una productora" - producer_field_error: "Por favor elige uno. ¿Eres una productora?" - yes_producer_help: "Las productoras hacen cosas deliciosas para comer y/o beber. Eres una productora si lo cultivas, lo haces crecer, lo preparas, lo horneas, lo fermentas, lo ordeñas, ..." - no_producer_help: "Si no eres una productora, probablemente conozcas a alguien que venda o distribuya comida. También podrías convertirte en un grupo de consumo u otro tipo de organización." about: title: 'Acerca de' images: @@ -1607,7 +1686,6 @@ es: registration_type_error: "Escoja una. ¿Es una productora?" registration_type_producer_help: "Las productoras hacen cosas deliciosas para comer y/o beber. Eres una productora si lo cultivas, lo haces crecer, lo preparas, lo horneas, lo fermentas, lo ordeñas, ..." registration_type_no_producer_help: "Si no eres una productora, probablemente conozcas a alguien que venda o distribuya comida. También podrías convertirte en un grupo de consumo u otro tipo de organización." - create_profile: "Crear Perfil" registration_images_headline: "¡Ya casi lo tenemos!" registration_images_description: "¡Sube algunas fotografías así el perfil se verá mucho mejor! :)" registration_detail_headline: "Empecemos..." @@ -1666,7 +1744,6 @@ es: you_have_no_orders_yet: "No tienes pedidos todavía" running_balance: "Saldo actual" outstanding_balance: "Saldo extraordinario" - admin_entreprise_relationships: "Relaciones de la Organización" admin_entreprise_relationships_everything: "Marcar todos" admin_entreprise_relationships_permits: "Permite" admin_entreprise_relationships_seach_placeholder: "Buscar" @@ -1749,12 +1826,7 @@ es: spree_admin_enterprises_fees: "Comisiones de la Organización" spree_admin_enterprises_none_create_a_new_enterprise: "CREAR NUEVA ORGANIZACIÓN" spree_admin_enterprises_none_text: "No tienes ninguna organización" - spree_admin_enterprises_producers_name: "Nombre" - spree_admin_enterprises_producers_total_products: "Total de Productos" - spree_admin_enterprises_producers_active_products: "Productos Activos" - spree_admin_enterprises_producers_order_cycles: "Productos en OCs" spree_admin_enterprises_tabs_hubs: "HUBS" - spree_admin_enterprises_tabs_producers: "PRODUCTORAS" spree_admin_enterprises_producers_manage_products: "GESTIONAR PRODUCTOS" spree_admin_enterprises_any_active_products_text: "No tienes ningún producto activo" spree_admin_enterprises_create_new_product: "CREAR UN NUEVO PRODUCTO" @@ -1795,11 +1867,7 @@ es: edit_profile_details_etc: "Cambia tu descripción, imágenes, etc." order_cycle: "Ciclo de Pedido" order_cycles: "Ciclos de Pedidos" - enterprises: "Organizaciones" - enterprise_relationships: "Relaciones entre organizaciones" remove_tax: "Eliminar impuesto" - enterprise_terms_of_service: "Términos del Servicio de la Organización" - enterprises_require_tos: "Las organizaciones deben aceptar los Términos del Servicio" enterprise_tos_link: "Enlace a los Términos del Servicio de la Organización" enterprise_tos_message: "Queremos trabajar con personas que compartan nuestros objetivos y valores. Por ello, pedimos a las nuevas organizaciones que acepten" enterprise_tos_link_text: "Términos del Servicio." @@ -2019,7 +2087,6 @@ es: content_configuration_pricing_table: "(TODO: tabla de precios)" content_configuration_case_studies: "(TODO: Casos de Estudio)" content_configuration_detail: "(TODO: Detalle)" - enterprise_name_error: "ya se ha utilizado. Si se trata de tu organización y deseas reclamar la propiedad, ponte en contacto con el administrador actual de este perfil en %{email}." enterprise_owner_error: "^ %{email} no está autorizado a tener más organizaciones (el límite es %{enterprise_limit})." enterprise_role_uniqueness_error: "^Este rol ya está presente." inventory_item_visibility_error: Debe ser verdadero o falso @@ -2201,6 +2268,8 @@ es: resolve: Resolver new_tag_rule_dialog: select_rule_type: "Selecciona un tipo de regla:" + resend_user_email_confirmation: + resend: "Reenviar" out_of_stock: reduced_stock_available: Stock reducido disponible out_of_stock_text: > @@ -2301,10 +2370,6 @@ es: account_id: Account ID business_name: Nombre de la Organización charges_enabled: Cargos habilitados - payments: - source_forms: - stripe: - no_payment_via_admin_backend: La creación de pagos basados ​​en Stripe desde el administrador no es posible en este momento. products: new: title: 'Nuevo producto' @@ -2321,10 +2386,10 @@ es: unit: Unidad display_as: Mostrar como category: Categoría - tax_category: Categoría del impuesto - inherits_properties?: Hereda propiedades? + tax_category: Categoría de impuestos + inherits_properties?: ¿Hereda propiedades? available_on: Disponible en - av_on: "Disp. en" + av_on: "Av. En" products_variant: variant_has_n_overrides: "Esta variante tiene %{n} override(s)" new_variant: "Nueva variante" @@ -2342,12 +2407,13 @@ es: bulk_coop_allocation: 'Bulk Co-op - Asignación' bulk_coop_packing_sheets: 'Bulk Co-op - Hojas de Empaquetado' bulk_coop_customer_payments: 'Bulk Co-op - Pagos de las Consumidoras' - shared: - configuration_menu: - stripe_connect: Conectar con Stripe variants: autocomplete: producer_name: Productora + general_settings: + edit: + enterprises_require_tos: "Las organizaciones deben aceptar los Términos del Servicio" + footer_tos_url: "URL de términos y servicios" checkout: payment: stripe: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 81b828e7e9..3b49c7ffbc 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -197,12 +197,12 @@ fr: actions: create_and_add_another: "Créer et ajouter nouveau" admin: - begins_at: Commence à + begins_at: Commence begins_on: Commence le customer: Acheteur date: Date email: Email - ends_at: Termine à + ends_at: Termine ends_on: Termine le name: Nom on_hand: En stock @@ -379,6 +379,7 @@ fr: producer_signup_page: Page d'inscription Producteur hub_signup_page: Page d'inscription Hub group_signup_page: Page d'inscription Groupe + main_links: Liens du menu principal footer_and_external_links: Pied de page et Liens Externes your_content: Votre contenu enterprise_fees: @@ -667,8 +668,8 @@ fr: allow_order_changes_true: "Les acheteurs peuvent modifier / valider leurs commandes tant que le cycle de vente est ouvert" enable_subscriptions: "Abonnements" enable_subscriptions_tip: "Activer la fonction abonnements?" - enable_subscriptions_false: "Désactivée" - enable_subscriptions_true: "Activée" + enable_subscriptions_false: "Désactivé" + enable_subscriptions_true: "Activé" shopfront_message: Message d'accueil boutique ouverte shopfront_message_placeholder: > Vous pouvez ici expliquer à vos acheteurs comment votre boutique fonctionne. @@ -1054,7 +1055,7 @@ fr: footer_legal_visit: "Nous trouver sur" footer_legal_text_html: "Open Food Network est une plateforme logicielle open source, libre et gratuite. Nos données sont protégées sous licence %{content_license} et notre code sous %{code_license}." footer_data_text_with_privacy_policy_html: "Nous prenons soin de vos données. Voir notre %{privacy_policy} et %{cookies_policy}." - footer_data_text_without_privacy_policy_html: "Nous prenons soin de vos données. Voire notre %{cookies_policy}." + footer_data_text_without_privacy_policy_html: "Nous prenons soin de vos données. Voir notre %{cookies_policy}." footer_data_privacy_policy: "politique de confidentialité" footer_data_cookies_policy: "politique de cookies" footer_skylight_dashboard_html: Les informations de performance sont disponibles sur %{dashboard}. @@ -1093,6 +1094,20 @@ fr: ticket_column_item: "Produit" ticket_column_unit_price: "Prix unitaire" ticket_column_total_price: "Prix total" + menu_1_title: "Boutiques" + menu_1_url: "/shops" + menu_2_title: "Carte" + menu_2_url: "/map" + menu_3_title: "Producteurs" + menu_3_url: "/producers" + menu_4_title: "Groupes" + menu_4_url: "/groups" + menu_5_title: "A propos" + menu_5_url: "https://apropos.openfoodfrance.org/" + menu_6_title: "Blog" + menu_6_url: "https://apropos.openfoodfrance.org/blog/" + menu_7_title: "Support" + menu_7_url: "https://apropos.openfoodfrance.org/support/" logo: "Logo (640x130)" logo_mobile: "Logo smartphone (75x26)" logo_mobile_svg: "Logo smartphone (SVG)" @@ -1190,18 +1205,16 @@ fr: essential_cookies: "Cookies essentiels" essential_cookies_desc: "Les cookies suivants sont nécessaires au fonctionnement du site openfoodfrance.org." essential_cookies_note: "Les cookies contiennent un identifiant unique, mais pas d'autres données. Vos emails et mots de passe par exemple ne sont jamais exposés dans les cookies!" - cookie_name: "Nom du cookie" - cookie_domain: "Déposé par" - cookie_desc: "Description" + cookie_domain: "Déposé par!" cookie_session_desc: "Utilisé pour garder en mémoire l'utilisateur d'une page à l'autre lors de la navigation sur le site, ou pour se souvenir des produits dans le panier." cookie_consent_desc: "Utilisé pour se souvenir du consentement de l'utilisation à l'utilisation de cookies." cookie_remember_me_desc: "Utilisé si l'utilisateur a cliqué sur \"se souvenir de moi\" (pour ne pas avoir à se reconnecter à chaque fois). Ce cookie est automatiquement supprimé après 12 jours. Si l'utilisateur souhaite supprimer ce cookie, il n'a qu'à se déconnecter. Si l'utilisateur ne souhaite pas que ce cookie soit installé sur son terminal, il suffit de ne pas cocher la case \"se souvenir de moi\" au moment de la connexion." cookie_openstreemap_desc: "Utilisé par le logiciel de cartographie open source et ami Open Street Map (qui permet l'affichage de la carte sur Open Food France) pour assurer qu'il ne reçoit pas trop de requêtes sur un laps de temps déterminé, et éviter ainsi les risques d'abus de leurs services." cookie_stripe_desc: "Utilisé par le terminal de payement en ligne Stripe (proposé aux utilisateurs d'Open Food France) https://stripe.com/fr/cookies-policy/legal. Même si toutes les boutiques n'utilisent pas Stripe, c'est une bonne pratique en matière de sécurité d'appliquer ce cookie sur toutes les pages vues. Stripe construit probablement une image des pages qui ont un quelconque lien avec l'API connectant Open Food France à leur système de paiement pour détecter les comportements anormaux pouvant suggérer un risque de fraude. Donc ce cookie a un rôle qui va au-delà de la simple fourniture d'un système de paiement. Le supprimer pourrait affecter la sécurité du service. Pour en savoir plus sur la politique de confidentialité de Stripe: https://stripe.com/fr/privacy." statistics_cookies: "Cookies d'analyse de navigation" - statistics_cookies_desc: "Ces cookies ne sont pas obligatoires, mais nous permettent de mieux comprendre votre usage de la plateforme, les endroits où vous bloquez, les fonctionnalités qui semble vous manquer, ou que vous n'utilisez jamais, afin de fournir le service le plus adapté possible aux besoins des utilisateurs." - statistics_cookies_analytics_desc: "Nous utilisons Google Analytics, pas vraiment par choix, mais simplement parce que c'était l'outil d'analyse connecté par défaut via Spree, le logiciel e-commerce open source sur lequel nous avons construit. Mais nous espérons pouvoir rapidement migrer vers Matomo (anciennement Piwik), outil d'analyse open source engagé sur le respect de la vie privée des utilisateurs." - statistics_cookies_matomo_desc: "Nous utilisons Matomo (anciennement Piwik), outil d'analyse open source engagé sur le respect de la vie privée des utilisateurs." + statistics_cookies_desc: "Ces cookies ne sont pas obligatoires, mais nous permettent de mieux comprendre votre usage de la plateforme, les endroits où vous bloquez, les fonctionnalités qui semblent vous manquer, ou que vous n'utilisez jamais, afin de fournir le service le plus adapté possible aux besoins des utilisateurs." + statistics_cookies_analytics_desc_html: "Pour analyser les données concernant votre usage de la plateforme, nous utilisons Google Analytics, pas vraiment par choix, mais simplement parce que c'était l'outil d'analyse connecté par défaut via Spree, le logiciel e-commerce open source sur lequel nous avons construit. Mais nous espérons pouvoir rapidement migrer vers Matomo (anciennement Piwik), outil d'analyse open source compatible RGPD et engagé sur le respect de la vie privée des utilisateurs." + statistics_cookies_matomo_desc_html: "Pour analyser les données concernant votre usage de la plateforme, nous utilisons Matomo(anciennement Piwik), outil d'analyse open source compatible RGPD et engagé sur le respect de la vie privée des utilisateurs." cookie_analytics_utma_desc: "Utilisé pour distinguer les utilisateurs et les sessions. Ce cookie est installé quand la librairie Javascript s'exécute et qu'aucun cookie __utma n'existe déjà. Le cookie est mis à jour à chaque fois que des données sont envoyées à Google Analytics." cookie_analytics_utmt_desc: "Utilisé pour limiter le taux de requêtes." cookie_analytics_utmb_desc: "Utilisé pour distinguer les nouvelles sessions/visites. Ce cookie est installé quand la librairie Javascript s'exécute et qu'aucun cookie __utmb n'existe déjà. Le cookie est mis à jour à chaque fois que des données sont envoyées à Google Analytics. " @@ -1215,9 +1228,15 @@ fr: disabling_cookies_firefox_link: "https://support.mozilla.org/fr/kb/activer-desactiver-cookies-preferences" disabling_cookies_chrome_link: "https://support.google.com/chrome/answer/95647?hl=fr" disabling_cookies_ie_link: "https://support.microsoft.com/fr-fr/help/17442/windows-internet-explorer-delete-manage-cookies" + disabling_cookies_safari_link: "https://www.apple.com/legal/privacy/fr-ww/cookies/" + disabling_cookies_note: "Mais gardez bien en tête que si vous supprimez ou modifiez un des cookies essentiels utilisés par Open Food France, le site ne fonctionnera pas correctement, vous ne pourrez pas ajouter des produits à votre panier ni finaliser votre commande par exemple." cookies_banner: + cookies_usage: "Ce site utilise des cookies pour rendre votre navigation fluide et sécurisée, et nous aider à comprendre l'usage que vous faites de la plateforme afin d'améliorer les fonctionnalités offertes." cookies_definition: "Les cookies sont de tout petits fichiers texte qui sont stockés sur votre ordinateur quand vous naviguez sur certains sites web." + cookies_desc: "Nous n'utilisons que les cookies nécessaires pour vous offrir un service de vente/achat de produits alimentaires en ligne performant. Nous ne vendons aucune de vos données. Nous utilisons ces cookies principalement pour vous permettre de rester connecté(e) après votre première connexion, ou encore pour mémoriser les produits dans votre panier lorsque vous n'êtes pas connecté(e). Si vous naviguez sur le site sans cliquer sur \"Accepter les cookies\", cela vaut consentement à l'utilisation des cookies nécessaires au bon fonctionnement du site." + cookies_policy_link_desc: "Si vous voulez en savoir plus, consultez notre" cookies_policy_link: "politique de cookies" + cookies_accept_button: "Accepter les cookies" home_shop: Faire mes courses brandstory_headline: "Des aliments porteurs de sens." brandstory_intro: "Parfois, le meilleur moyen de réparer le système, c'est d'en inventer un autre..." @@ -1599,6 +1618,7 @@ fr: error_number: "saisir un nombre" error_email: "saisir une adresse email" error_not_found_in_database: "%{name} n'existe pas" + error_not_primary_producer: "%{name}n'est pas enregistré comme \"producteur\"" error_no_permission_for_enterprise: "\"%{name}\" : vous n'avez pas les droits requis pour gérer les produits de cette entreprise" item_handling_fees: "Frais logistiques (inclus dans le prix affiché)" january: "Janvier" @@ -2469,7 +2489,8 @@ fr: payments: source_forms: stripe: - no_payment_via_admin_backend: La création de paiements via Stripe depuis le back office d'administration n'est pas possible pour le moment + error_saving_payment: Erreur à l'enregistrement du paiement + submitting_payment: Envoi du paiement... products: new: title: 'Nouveau Produit' @@ -2518,7 +2539,12 @@ fr: producer_name: Producteur general_settings: edit: + legal_settings: "Configuration légales" + cookies_consent_banner_toggle: "Afficher la bannière de consentement à l'utilisation des cookies" + privacy_policy_url: "URL de la politique de confidentialité" enterprises_require_tos: "Les entreprises doivent accepter les Conditions Générales d'Utilisation" + cookies_policy_matomo_section: "Afficher la section Matomo sur la politique de cookies" + cookies_policy_ga_section: "Afficher la section Google Analytics sur la politique de cookies" footer_tos_url: "Conditions d'utilisation URL" checkout: payment: diff --git a/config/locales/fr_CA.yml b/config/locales/fr_CA.yml index 1c30003635..e14cf54d3e 100644 --- a/config/locales/fr_CA.yml +++ b/config/locales/fr_CA.yml @@ -455,6 +455,7 @@ fr_CA: inventory_template: Télécharger le modèle pour import dans catalogue boutique category_values: Valeurs disponibles pour les catégories product_categories: Catégorie Produit + tax_categories: Taxe applicable shipping_categories: Condition de transport import: review: Vérifier @@ -1092,6 +1093,13 @@ fr_CA: ticket_column_item: "Produit" ticket_column_unit_price: "Prix unitaire" ticket_column_total_price: "Prix total" + menu_1_title: "Boutiques" + menu_2_title: "Carte" + menu_3_title: "Producteurs" + menu_4_title: "Groupes" + menu_5_title: "A propos" + menu_6_title: "Se connecter" + menu_7_title: "Apprendre" logo: "Logo (640x130)" logo_mobile: "Logo smartphone (75x26)" logo_mobile_svg: "Logo smartphone (SVG)" @@ -1189,18 +1197,12 @@ fr_CA: essential_cookies: "Cookies essentiels" essential_cookies_desc: "Les cookies suivants sont nécessaires au fonctionnement du site openfoodfrance.org." essential_cookies_note: "Les cookies contiennent un identifiant unique, mais pas d'autres données. Vos emails et mots de passe par exemple ne sont jamais exposés dans les cookies!" - cookie_name: "Nom du cookie" - cookie_domain: "Déposé par" - cookie_desc: "Description" cookie_session_desc: "Utilisé pour garder en mémoire l'utilisateur d'une page à l'autre lors de la navigation sur le site, ou pour se souvenir des produits dans le panier." cookie_consent_desc: "Utilisé pour se souvenir du consentement de l'utilisation à l'utilisation de cookies." cookie_remember_me_desc: "Utilisé si l'utilisateur a cliqué sur \"se souvenir de moi\" (pour ne pas avoir à se reconnecter à chaque fois). Ce cookie est automatiquement supprimé après 12 jours. Si l'utilisateur souhaite supprimer ce cookie, il n'a qu'à se déconnecter. Si l'utilisateur ne souhaite pas que ce cookie soit installé sur son terminal, il suffit de ne pas cocher la case \"se souvenir de moi\" au moment de la connexion." cookie_openstreemap_desc: "Utilisé par le logiciel de cartographie open source pour assurer qu'il ne reçoit pas trop de requêtes sur un laps de temps déterminé, et éviter ainsi les risques d'abus de leurs services." cookie_stripe_desc: "Utilisé par le terminal de payement en ligne Stripe (proposé aux utilisateurs d'Open Food France) https://stripe.com/fr/cookies-policy/legal. Même si toutes les boutiques n'utilisent pas Stripe, c'est une bonne pratique en matière de sécurité d'appliquer ce cookie sur toutes les pages vues. Stripe construit probablement une image des pages qui ont un quelconque lien avec l'API connectant Open Food France à leur système de paiement pour détecter les comportements anormaux pouvant suggérer un risque de fraude. Donc ce cookie a un rôle qui va au-delà de la simple fourniture d'un système de paiement. Le supprimer pourrait affecter la sécurité du service. Pour en savoir plus sur la politique de confidentialité de Stripe: https://stripe.com/fr/privacy." statistics_cookies: "Cookies d'analyse de navigation" - statistics_cookies_desc: "Ces cookies ne sont pas obligatoires, mais nous permettent de mieux comprendre votre usage de la plateforme, les endroits où vous bloquez, les fonctionnalités qui semble vous manquer, ou que vous n'utilisez jamais, afin de fournir le service le plus adapté possible aux besoins des utilisateurs." - statistics_cookies_analytics_desc: "Nous utilisons Google Analytics, pas vraiment par choix, mais simplement parce que c'était l'outil d'analyse connecté par défaut via Spree, le logiciel e-commerce open source sur lequel nous avons construit. Mais nous espérons pouvoir rapidement migrer vers Matomo (anciennement Piwik), outil d'analyse open source engagé sur le respect de la vie privée des utilisateurs." - statistics_cookies_matomo_desc: "Nous utilisons Matomo (anciennement Piwik), outil d'analyse open source engagé sur le respect de la vie privée des utilisateurs." cookie_analytics_utma_desc: "Utilisé pour distinguer les utilisateurs et les sessions. Ce cookie est installé quand la librairie Javascript s'exécute et qu'aucun cookie __utma n'existe déjà. Le cookie est mis à jour à chaque fois que des données sont envoyées à Google Analytics." cookie_analytics_utmt_desc: "Utilisé pour limiter le taux de requêtes." cookie_analytics_utmb_desc: "Utilisé pour distinguer les nouvelles sessions/visites. Ce cookie est installé quand la librairie Javascript s'exécute et qu'aucun cookie __utmb n'existe déjà. Le cookie est mis à jour à chaque fois que des données sont envoyées à Google Analytics. " @@ -1210,12 +1212,19 @@ fr_CA: cookie_matomo_heatmap_desc: "Utilisé par Matomo pour enregistrer les sessions et \"cartes thermiques\" (représentations graphiques des données)" cookie_matomo_ignore_desc: "Cookie utilisé pour se souvenir qu'un utilisateur a souhaité explicitement que sa navigation ne soit pas analysée par Matomo, et exclure cet utilisateur du suivi du site." disabling_cookies_header: "Mises en garde sur la désactivation des cookies" + disabling_cookies_desc: "En tant qu'utilisateur, vous pouvez toujours autoriser, bloquer ou supprimer tous les cookies utilisés par Open Food Network Canada ou tout autre site web via les paramètres de votre navigateur. Chaque navigateur a un chemin spécifique pour effectuer cette désactivation:" + disabling_cookies_firefox_link: "https://support.mozilla.org/en-US/kb/enable-and-disable-cookies-website-preferences" + disabling_cookies_chrome_link: "https://support.google.com/chrome/answer/95647" + disabling_cookies_ie_link: "https://support.microsoft.com/en-us/help/17442/windows-internet-explorer-delete-manage-cookies" + disabling_cookies_safari_link: "https://www.apple.com/legal/privacy/en-ww/cookies/" disabling_cookies_note: "Mais gardez bien en tête que si vous supprimez ou modifiez un des cookies essentiels utilisés par Open Food France, le site ne fonctionnera pas correctement, vous ne pourrez pas ajouter des produits à votre panier ni finaliser votre commande par exemple." cookies_banner: cookies_usage: "Ce site utilise des cookies pour rendre votre navigation fluide et sécurisée, et nous aider à comprendre l'usage que vous faites de la plateforme afin d'améliorer les fonctionnalités offertes." cookies_definition: "Les cookies sont de tout petits fichiers texte qui sont stockés sur votre ordinateur quand vous naviguez sur certains sites web." cookies_desc: "Nous n'utilisons que les cookies nécessaires pour vous offrir un service de vente/achat de produits alimentaires en ligne performant. Nous ne vendons aucune de vos données. Nous utilisons ces cookies principalement pour vous permettre de rester connecté(e) après votre première connexion, ou encore pour mémoriser les produits dans votre panier lorsque vous n'êtes pas connecté(e). Si vous naviguez sur le site sans cliquer sur \"Accepter les cookies\", cela vaut consentement à l'utilisation des cookies nécessaires au bon fonctionnement du site." + cookies_policy_link_desc: "Si vous voulez en savoir plus, consultez notre" cookies_policy_link: "politique de cookies" + cookies_accept_button: "Accepter les cookies" home_shop: Faire mes courses brandstory_headline: "Des aliments porteurs de sens." brandstory_intro: "Parfois, le meilleur moyen de réparer le système, c'est d'en inventer un autre..." @@ -2464,10 +2473,6 @@ fr_CA: account_id: Identifiant Compte business_name: Nom de l'entreprise charges_enabled: Frais activés - payments: - source_forms: - stripe: - no_payment_via_admin_backend: La création de paiements via Stripe depuis le back office d'administration n'est pas possible pour le moment products: new: title: 'Nouveau Produit' @@ -2516,6 +2521,9 @@ fr_CA: producer_name: Producteur general_settings: edit: + legal_settings: "Configuration légales" + cookies_consent_banner_toggle: "Afficher la bannière de consentement à l'utilisation des cookies" + privacy_policy_url: "URL de la politique de confidentialité" enterprises_require_tos: "Les entreprises doivent accepter les Conditions Générales d'Utilisation" cookies_policy_matomo_section: "Afficher la section Matomo sur la politique de cookies" cookies_policy_ga_section: "Afficher la section Google Analytics sur la politique de cookies" diff --git a/config/locales/it.yml b/config/locales/it.yml index fe81ff7f31..24eb43f696 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -91,6 +91,8 @@ it: end_date: "Data di fine" clear_filters: Cancella filtri clear: Pulisci + cancel: Annulla + back: Indietro columns: Colonne actions: Azioni viewing: "Vista: %{current_view_name}" @@ -103,6 +105,12 @@ it: accounts_and_billing_settings: edit: admin_settings: "Impostazioni" + cache_settings: + show: + error: Eorrore + stripe_connect_settings: + edit: + settings: "Impostazioni" customers: index: add_customer: "Aggiungi cliente" @@ -122,11 +130,19 @@ it: select_country: 'Seleziona il paese' select_state: 'Selezione la provincia' edit: 'Modifica' - cache_settings: - show: - error: Eorrore products: + index: + unit: Unità + display_as: Visualizza come + category: Categoria + tax_category: Categoria di imposta + inherits_properties?: Eredita proprietà? + available_on: Disponibile il + av_on: "Disp. il" Search: Cerca + product_import: + model: + blank: non può essere lasciato vuoto variant_overrides: index: title: Inventario @@ -182,17 +198,43 @@ it: cancel: Annulla users: contact: "Contatto" + actions: + edit_profile: Impostazioni + enterprise_user_index: + manage_link: Impostazioni + order_cycles: + date_warning: + cancel: Annulla subscriptions: review: address: Indirizzo products: Prodotti - stripe_connect_settings: - edit: - settings: "Impostazioni" shared: register_call: selling_on_ofn: "Interessato ad entrare in Open Food Network?" register: "Registrati qui" + footer: + footer_global_headline: "OFN Globale" + footer_global_home: "Home" + footer_global_news: "News" + footer_global_about: "About" + footer_global_contact: "Contatto" + footer_sites_headline: "Siti OFN" + footer_sites_developer: "Sviluppatore" + footer_sites_community: "Community" + footer_sites_userguide: "Guida utente" + footer_secure: "Sicuro e affidabile." + footer_secure_text: "Open Food Network usa la codifica SSL (2048 bit RSA) ovunque per mantenere private le tue informazioni sulla spesa e i pagamenti. I nostri server non memorizzano i dettagli della tua carta di credito e i pagamenti sono processati da servizi PCI-conformi." + footer_contact_headline: "Tieniti in contatto" + footer_contact_email: "Scrivici" + footer_nav_headline: "Naviga" + footer_join_headline: "Unisciti a noi" + footer_join_body: "Crea una lista, un negozio o un gruppo su Open Food Network." + footer_join_cta: "Dimmi di più!" + footer_legal_call: "Leggi i nostri" + footer_legal_tos: "Termini e condizioni" + footer_legal_visit: "Trovaci su" + footer_legal_text_html: "Open Food Network è un software gratuito e open source. Il nostro contenuto è distribuito con licenza %{content_license} e il nostro codice con licenza %{code_license}." shop: messages: login: "fai il login" @@ -205,6 +247,13 @@ it: invoice_column_price: "Prezzo" invoice_column_item: "Articolo" invoice_column_qty: "Qtà." + menu_1_title: "Negozi" + menu_2_title: "Mappa" + menu_3_title: "Produttori" + menu_4_title: "Gruppi" + menu_5_title: "About" + menu_6_title: "Connetti" + menu_7_title: "Impara" logo: "Logo (640x130)" logo_mobile: "Mobile logo (75x26)" logo_mobile_svg: "Mobile logo (SVG)" @@ -220,7 +269,6 @@ it: footer_email: "Email" footer_links_md: "Collegamenti" footer_about_url: "URL About" - footer_tos_url: "URL Termini di Servizio" name: Nome first_name: Nome last_name: Cognome @@ -268,27 +316,6 @@ it: ie_warning_firefox: Scarica Firefox ie_warning_ie: Aggiorna Internet Explorer ie_warning_other: "Non puoi aggiornare il browser? Prova Open Food Network sul tuo smartphone :-)" - footer_global_headline: "OFN Globale" - footer_global_home: "Home" - footer_global_news: "News" - footer_global_about: "About" - footer_global_contact: "Contatta" - footer_sites_headline: "Siti OFN" - footer_sites_developer: "Sviluppatore" - footer_sites_community: "Community" - footer_sites_userguide: "Guida utente" - footer_secure: "Sicuro e affidabile." - footer_secure_text: "Open Food Network usa la codifica SSL (2048 bit RSA) ovunque per mantenere private le tue informazioni sulla spesa e i pagamenti. I nostri server non memorizzano i dettagli della tua carta di credito e i pagamenti sono processati da servizi PCI-conformi." - footer_contact_headline: "Tieniti in contatto" - footer_contact_email: "Scrivici" - footer_nav_headline: "Naviga" - footer_join_headline: "Unisciti a noi" - footer_join_body: "Crea una lista, un negozio o un gruppo su Open Food Network." - footer_join_cta: "Dimmi di più!" - footer_legal_call: "Leggi i nostri" - footer_legal_tos: "Termini e condizioni" - footer_legal_visit: "Trovaci su" - footer_legal_text_html: "Open Food Network è un software gratuito e open source. Il nostro contenuto è distribuito con licenza %{content_license} e il nostro codice con licenza %{code_license}." home_shop: Compra ora brandstory_headline: "Cibo, libero." brandstory_intro: "A volte il modo migliore di correggere il sistema è crearne uno nuovo..." @@ -640,6 +667,17 @@ it: password_reset_sent: "Ti abbiamo mandato una email con le istruzioni per resettare la password." reset_password: "Resetta la password" who_is_managing_enterprise: "Chi è responsabile per la gestione di %{enterprise}?" + registration: + steps: + type: + headline: "Ultimo passo per aggiungere %{enterprise}!" + question: "Sei un produttore?" + yes_producer: "Sì, sono un produttore" + no_producer: "No, non sono un produttore" + producer_field_error: "Per favore scegline uno. Sei un produttore?" + yes_producer_help: "I produttori fanno cose buone da mangiare e/o bere. Sei un produttore se le coltivi, le allevi, le infondi, le cucini, le fai fermentare, le mungi o le modelli." + no_producer_help: "Se non sei un produttore, probabilmente sei qualcuno che vende e distribuisce cibo. potresti essere un hub, una cooperativa, un gruppo d'acquisto, un rivenditore al dettaglio o all'ingrosso, o altro." + create_profile: "Crea profilo" enterprise_contact: "Contatto principale" enterprise_contact_required: "Devi inserire un contatto principale" enterprise_email_address: "Indirizzo email" @@ -716,7 +754,6 @@ it: registration_type_error: "Per favore scegline uno. Sei un produttore?" registration_type_producer_help: "I produttori fanno cose buone da mangiare e/o bere. Sei un produttore se le coltivi, le allevi, le infondi, le cucini, le fai fermentare, le mungi o le modelli." registration_type_no_producer_help: "Se non sei un produttore, probabilmente sei qualcuno che vende e distribuisce cibo. potresti essere un hub, una cooperativa, un gruppo d'acquisto, un rivenditore al dettaglio o all'ingrosso, o altro." - create_profile: "Crea profilo" registration_images_headline: "Grazie!" registration_images_description: "Carichiamo qualche immagine per rendere più bello il tuo profilo! :)" registration_detail_headline: "Cominciamo" @@ -770,7 +807,6 @@ it: you_have_no_orders_yet: "Non hai ordini al momento" running_balance: "Bilancio corrente" outstanding_balance: "Insoluto" - admin_entreprise_relationships: "Relazioni dell'azienda" admin_entreprise_relationships_everything: "Tutto" admin_entreprise_relationships_permits: "permessi" admin_entreprise_relationships_seach_placeholder: "Cerca" @@ -836,12 +872,7 @@ it: spree_admin_enterprises_fees: "Contributi dell'azienda" spree_admin_enterprises_none_create_a_new_enterprise: "CREA NUOVA AZIENDA" spree_admin_enterprises_none_text: "Non hai nessua azienda al momento" - spree_admin_enterprises_producers_name: "Nome" - spree_admin_enterprises_producers_total_products: "Totale prodotti" - spree_admin_enterprises_producers_active_products: "Prodotti attivi" - spree_admin_enterprises_producers_order_cycles: "Prodotti in OC" spree_admin_enterprises_tabs_hubs: "HUBS" - spree_admin_enterprises_tabs_producers: "PRODUTTORI" spree_admin_enterprises_producers_manage_products: "GESTISCI PRODOTTI" spree_admin_enterprises_any_active_products_text: "Non hai prodotti attivi." spree_admin_enterprises_create_new_product: "CREA UN NUOVO PRODOTTO" @@ -942,7 +973,24 @@ it: panels: enterprise_status: description: Descrizione + resend_user_email_confirmation: + resend: "Invia nuovamente" spree: + admin: + products: + index: + products_head: + name: Nome + unit: Unità + display_as: Visualizza come + category: Categoria + tax_category: Categoria di imposta + inherits_properties?: Eredita proprietà? + available_on: Disponibile il + av_on: "Disp. il" + general_settings: + edit: + footer_tos_url: "URL Termini di Servizio" order_state: address: indirizzo adjustments: aggiustamenti diff --git a/config/locales/nb.yml b/config/locales/nb.yml index 5cb20bd7bb..bfb7ad6468 100644 --- a/config/locales/nb.yml +++ b/config/locales/nb.yml @@ -379,6 +379,7 @@ nb: producer_signup_page: Produsent registreringsside hub_signup_page: Hub-registreringsside group_signup_page: Grupperegistreringsside + main_links: 'Lenker Hovedmeny ' footer_and_external_links: Footer og eksterne lenker your_content: Ditt innhold enterprise_fees: @@ -1030,6 +1031,33 @@ nb: register_call: selling_on_ofn: "Interessert i å bli med i Open Food Network?" register: "Registrer her" + footer: + footer_global_headline: "OFN Globalt" + footer_global_home: "Hjem" + footer_global_news: "Nyheter" + footer_global_about: "Om" + footer_global_contact: "Kontakt" + footer_sites_headline: "OFN nettsteder" + footer_sites_developer: "Utvikler" + footer_sites_community: "Forum" + footer_sites_userguide: "Brukermanual" + footer_secure: "Sikker og klarert." + footer_secure_text: "Open Food Network bruker SSL-kryptering (2048 bit RSA) overalt for å holde handlingen og betalingen din privat. Våre servere lagrer ikke kortopplysninger og betalinger behandles av PCI-kompatible tjenester." + footer_contact_headline: "Hold kontakten" + footer_contact_email: "Send oss epost" + footer_nav_headline: "Naviger" + footer_join_headline: "Bli med" + footer_join_body: "Opprette en profil, butikk eller gruppe på Open Food Network." + footer_join_cta: "Fortell meg mer!" + footer_legal_call: "Les våre" + footer_legal_tos: "Vilkår og betingelser" + footer_legal_visit: "Finn oss på" + footer_legal_text_html: "Open Food Network er en plattform med fri og åpen kildekode. Vårt innhold er lisensiert med %{content_license} og vår kode med %{code_license}." + footer_data_text_with_privacy_policy_html: "Vi tar godt vare på dine data. Se vår %{privacy_policy} og %{cookies_policy}" + footer_data_text_without_privacy_policy_html: "Vi tar godt vare på dine data. Se vår %{cookies_policy}" + footer_data_privacy_policy: "personvernpolicy" + footer_data_cookies_policy: "policy informasjonskapsler" + footer_skylight_dashboard_html: Ytelsesdata er tilgjengelig på %{dashboard}. shop: messages: login: "innlogging" @@ -1065,6 +1093,20 @@ nb: ticket_column_item: "Vare" ticket_column_unit_price: "Enhetspris" ticket_column_total_price: "Totalpris" + menu_1_title: "Butikker" + menu_1_url: "/shops" + menu_2_title: "Kart" + menu_2_url: "/map" + menu_3_title: "Produsenter" + menu_3_url: "/producers" + menu_4_title: "Grupper" + menu_4_url: "/groups" + menu_5_title: "Om" + menu_5_url: "https://openfoodnetwork.org/ofn-local/open-food-network-scandinavia/" + menu_6_title: "Koble til" + menu_6_url: " " + menu_7_title: "Lære" + menu_7_url: " " logo: "Logo (640x130)" logo_mobile: "Mobil logo (75x26)" logo_mobile_svg: "Mobil logo (SVG)" @@ -1080,7 +1122,6 @@ nb: footer_email: "Epost" footer_links_md: "Linker" footer_about_url: "Om URL" - footer_tos_url: "Vilkår URL" name: Navn first_name: Fornavn last_name: Etternavn @@ -1154,28 +1195,47 @@ nb: ie_warning_firefox: Last ned Firefox ie_warning_ie: Oppgrader Internet Explorer ie_warning_other: "Kan ikke oppgradere nettleseren din? Prøv Open Food Network på smart-telefonen din :-)" - footer_global_headline: "OFN Globalt" - footer_global_home: "Hjem" - footer_global_news: "Nyheter" - footer_global_about: "Om" - footer_global_contact: "Kontakt" - footer_sites_headline: "OFN nettsteder" - footer_sites_developer: "Utvikler" - footer_sites_community: "Forum" - footer_sites_userguide: "Brukerhåndbok" - footer_secure: "Sikker og klarert." - footer_secure_text: "Open Food Network bruker SSL-kryptering (2048 bit RSA) overalt for å holde handlingen og betalingen din privat. Våre servere lagrer ikke kortopplysninger og betalinger behandles av PCI-kompatible tjenester." - footer_contact_headline: "Hold kontakten" - footer_contact_email: "Send oss en epost" - footer_nav_headline: "Naviger" - footer_join_headline: "Bli med" - footer_join_body: "Opprette en profil, butikk eller gruppe på Open Food Network." - footer_join_cta: "Fortell meg mer!" - footer_legal_call: "Les våre" - footer_legal_tos: "Vilkår og betingelser" - footer_legal_visit: "Finn oss på" - footer_legal_text_html: "Open Food Network er en plattform med fri og åpen kildekode. Vårt innhold er lisensiert med %{content_license} og vår kode med %{code_license}." - footer_skylight_dashboard_html: Ytelsesdata er tilgjengelig på %{dashboard}. + legal: + cookies_policy: + header: "Hvordan vi bruker informasjonskapsler" + desc_part_1: "Informasjonskapsler er svært små tekstfiler som er lagret på datamaskinen din når du besøker noen nettsteder." + desc_part_2: "I OFN respekterer vi fullt ut ditt privatliv. Vi bruker bare informasjonskapsler som er nødvendige for å levere deg tjenesten til å selge/kjøpe mat på nettet. Vi selger ikke noen av dataene dine. Vi kan i fremtiden foreslå at du deler noen av dataene dine for å bygge nye commons-tjenester som kan være nyttige for økosystemet (som logistikk-tjenester for kortmatssystemer), men vi er ikke der ennå, og vi vil ikke gjøre det uten din godkjenning :-)" + desc_part_3: "Vi bruker informasjonskapsler hovedsakelig for å huske hvem du er hvis du logger inn på tjenesten, eller for å kunne huske elementene du legger inn i handlekurven din selv om du ikke er logget inn. Hvis du fortsetter å navigere på nettstedet uten å klikke på \"Godta cookies\", antar vi at du gir oss samtykke til å lagre informasjonskapslene som er avgjørende for nettstedet. Her er listen over informasjonskapsler vi bruker!" + essential_cookies: "Viktige informasjonskapsler" + essential_cookies_desc: "Følgende informasjonskapsler er strengt nødvendige for driften av nettstedet vårt." + essential_cookies_note: "De fleste informasjonskapsler inneholder bare en unik identifikator, men ingen andre data, slik at epostadressen og passordet ditt for eksempel aldri er inneholdt i dem og aldri blir eksponert." + cookie_domain: "Satt av:" + cookie_session_desc: "Brukes til å tillate nettstedet å huske brukere mellom sidebesøk, for eksempel, husk elementer i handlekurven din." + cookie_consent_desc: "Brukes til å opprettholde status for brukerens samtykke til å lagre informasjonskapsler" + cookie_remember_me_desc: "Brukes hvis brukeren har bedt nettsiden om å huske ham. Denne informasjonskapsel slettes automatisk etter 12 dager. Hvis du som bruker ønsker at cookien skal slettes, trenger du bare å logge ut. Hvis du ikke vil at cookien skal installeres på datamaskinen, bør du ikke merke avkrysningsruten «husk meg» når du logger inn." + cookie_openstreemap_desc: "Brukes av vår vennlige open source kartleverandør (OpenStreetMap) for å sikre at den ikke mottar for mange forespørsler i en gitt tidsperiode for å forhindre misbruk av sine tjenester." + cookie_stripe_desc: "Data samlet inn av betalingsprosessoren vår Stripe for svindeloppdagelse https://stripe.com/cookies-policy/legal. Ikke alle butikker bruker Stripe som betalingsmetode, men det er en god praksis å forhindre at svindel gjelder for alle sider. Stripe bygger sannsynligvis et bilde av hvilke av våre sider som til vanlig samhandler med API-en og deretter flagge alt uvanlig. Så å sette Stripe-cookien har en bredere funksjon enn bare å levere en betalingsmetode til en bruker. Fjerning av det kan påvirke sikkerheten til selve tjenesten. Du kan lære mer om Stripe og lese retningslinjene for personvern på https://stripe.com/privacy." + statistics_cookies: "Statistikkkapsler" + statistics_cookies_desc: "Følgende er ikke strengt nødvendige, men hjelper deg med å gi deg den beste brukeropplevelsen ved å tillate oss å analysere brukeradferd, identifisere hvilke funksjoner du bruker mest, eller ikke bruker, forstå brukeropplevelsesproblemer osv." + statistics_cookies_analytics_desc_html: "For å samle og analysere plattformbruksdata bruker vi Google Analytics, da det var standardtjenesten som var koblet til Spree (ehandel open source programvare som vi bygde på), men visjonen vår er å bytte til Matomo (ex Piwik, open source analyseverktøy som er GDPR-kompatibelt og beskytter ditt privatliv) så snart vi kan." + statistics_cookies_matomo_desc_html: "For å samle og analysere plattformbruksdata bruker vi Matomo (ex Piwik), et åpen kildekodeanalyseverktøy som er kompatibelt med GDPR og beskytter personvernet ditt." + cookie_analytics_utma_desc: "Brukes til å skille mellom brukere og økter. Kapselen er opprettet når javascriptbiblioteket utføres, og ingen eksisterende __utma-informasjonskapsler eksisterer. Cookien oppdateres hver gang data sendes til Google Analytics." + cookie_analytics_utmt_desc: "Brukes til pådragsforespørselsrate." + cookie_analytics_utmb_desc: "Brukes til å bestemme nye økter/besøk. Kapselen blir opprettet når javascriptbiblioteket kjøres, og ingen eksisterende __utmb-cookies eksisterer. Cookien oppdateres hver gang data sendes til Google Analytics." + cookie_analytics_utmc_desc: "Ikke brukt i ga.js. Satt for interoperabilitet med urchin.js. Historisk kjørte denne informasjonskapselen sammen med __utmb-cookien for å avgjøre om brukeren var i en ny økt/besøk." + cookie_analytics_utmz_desc: "Lagrer trafikkilden eller kampanjen som forklarer hvordan brukeren nådde nettstedet ditt. Kapselen blir opprettet når javascriptbiblioteket utføres og oppdateres hver gang data sendes til Google Analytics." + cookie_matomo_basics_desc: "Matomo førstehånds kapsler for å samle statistikk." + cookie_matomo_heatmap_desc: "Matomo Heatmap & Session opptakskapsel." + cookie_matomo_ignore_desc: "Kapsel brukes til å utelukke at bruker blir sporet." + disabling_cookies_header: "Advarsel om deaktivering av informasjonskapsler" + disabling_cookies_desc: "Som bruker kan du alltid tillate, blokkere eller slette Open Food Network eller andre nettsiders kapsler når du vil gjennom nettleserens innstillingskontroll. Hver nettleser har sin egne måte å operere på. Her er linkene:" + disabling_cookies_firefox_link: "https://support.mozilla.org/en-US/kb/enable-and-disable-cookies-website-preferences" + disabling_cookies_chrome_link: "https://support.google.com/chrome/answer/95647" + disabling_cookies_ie_link: "https://support.microsoft.com/en-us/help/17442/windows-internet-explorer-delete-manage-cookies" + disabling_cookies_safari_link: "https://www.apple.com/legal/privacy/en-ww/cookies/" + disabling_cookies_note: "Men vær oppmerksom på at hvis du sletter eller endrer de essensielle informasjonskapslene som brukes av Open Food Network, vil nettstedet ikke fungere, du vil ikke kunne legge til noe i handlekurven din eller for eksempel for å sjekke ut." + cookies_banner: + cookies_usage: "Dette nettstedet bruker informasjonskapsler for å gjøre navigasjonen friksjonsfri og sikker, og for å hjelpe oss å forstå hvordan du bruker den for å forbedre funksjonene vi tilbyr." + cookies_definition: "Informasjonskapsler er svært små tekstfiler som er lagret på datamaskinen din når du besøker noen nettsteder." + cookies_desc: "Vi bruker bare informasjonskapsler som er nødvendige for å levere deg tjenesten til å selge/kjøpe mat på nettet. Vi selger ikke noen av dataene dine. Vi bruker informasjonskapsler hovedsakelig for å huske hvem du er hvis du logger inn på tjenesten, eller for å kunne huske elementene du legger inn i handlekurven din selv om du ikke er logget inn. Hvis du fortsetter å navigere på nettstedet uten å klikke på \"Godta cookies\", antar vi at du gir oss samtykke til å lagre informasjonskapslene som er avgjørende for nettstedet." + cookies_policy_link_desc: "Hvis du vil lære mer, sjekk vår" + cookies_policy_link: "policy om informasjonskapsler" + cookies_accept_button: "Godta Informasjonskapsler" home_shop: Handle nå brandstory_headline: "Food, unincorporated." brandstory_intro: "Noen ganger er det best å fikse systemet ved å starte et nytt..." @@ -1296,6 +1356,7 @@ nb: email_so_edit_true_html: "Du kan gjøre endringer til bestillinger lukkes på %{orders_close_at}." email_so_edit_false_html: "Du kan se detaljer på denne bestillingen når som helst." email_so_contact_distributor_html: "Hvis du har spørsmål, kan du kontakte %{distributor} via %{email}." + email_so_contact_distributor_to_change_order_html: "Denne bestillingen ble automatisk opprettet for deg. Du kan gjøre endringer til bestillinger stenger på %{orders_close_at} ved å kontakte %{distributor} via %{email}." email_so_confirmation_intro_html: "Bestillingen din med %{distributor} er nå bekreftet" email_so_confirmation_explainer_html: "Denne bestillingen ble automatisk lagt inn for deg, og den er nå fullført." email_so_confirmation_details_html: "Her er alt du trenger å vite om bestillingen din fra %{distributor} :" @@ -1557,6 +1618,7 @@ nb: error_number: "må være tall" error_email: "må være epostadresse" error_not_found_in_database: "%{name} ikke funnet i databasen" + error_not_primary_producer: "%{name} er ikke aktivert som produsent" error_no_permission_for_enterprise: "\"%{name}\": du har ikke rettigheter til å administrere produkter for denne bedriften" item_handling_fees: "Håndteringsavgifter for varen (inkludert i varens totaler)" january: "januar" @@ -1675,6 +1737,7 @@ nb: enterprise_about_headline: "Bra!" enterprise_about_message: "Nå la oss finne ut detaljene om" enterprise_success: "Suksess! %{enterprise} lagt til Open Food Network" + enterprise_registration_exit_message: "Hvis du avslutter denne veiviseren på et hvilket som helst tidspunkt, kan du fortsette å opprette profilen din ved å gå til administrasjonsgrensesnittet." enterprise_description: "Kort beskrivelse" enterprise_description_placeholder: "En kort setning som beskriver virksomheten din" enterprise_long_desc: "Lang beskrivelse" @@ -1782,6 +1845,7 @@ nb: you_have_no_orders_yet: "Du har ingen bestilinger enda" running_balance: "Løpende balanse" outstanding_balance: "Utestående balanse" + admin_entreprise_relationships: "Bedriftsrettigheter" admin_entreprise_relationships_everything: "Alt" admin_entreprise_relationships_permits: "tillater" admin_entreprise_relationships_seach_placeholder: "Søk" @@ -1905,9 +1969,8 @@ nb: edit_profile_details_etc: "Endre din profilbeskrivelse, bilder, osv." order_cycle: "Bestillingsrunde" order_cycles: "Bestillingsrunder" + enterprise_relationships: "Bedriftsrettigheter" remove_tax: "Fjern avgift" - enterprise_terms_of_service: "Tjenestevilkår for Bedrifter" - enterprises_require_tos: "Bedrifter må godta Tjenestevilkår" enterprise_tos_link: "Lenke til Tjenestevilkår for Bedrifter" enterprise_tos_message: "Vi ønsker å jobbe med bedrifter som deler våre mål og verdier. Derfor ber vi nye bedrifter om å godta vår" enterprise_tos_link_text: "Tjenestevilkår." @@ -2309,6 +2372,9 @@ nb: select_rule_type: "Velg en regeltype:" resend_user_email_confirmation: resend: "Send på nytt" + sending: "Send på nytt ..." + done: "Sending på nytt ferdig ✓" + failed: "Sending på nytt mislyktes ✗" out_of_stock: reduced_stock_available: Redusert lager tilgjengelig out_of_stock_text: > @@ -2411,7 +2477,8 @@ nb: payments: source_forms: stripe: - no_payment_via_admin_backend: Å skape Stripe-baserte betalinger fra admin-backend er ikke mulig for øyeblikket + error_saving_payment: Feil ved lagring av betaling + submitting_payment: Sender inn betaling... products: new: title: 'Nytt produkt' @@ -2445,14 +2512,28 @@ nb: display_as: display_as: Vis som reports: + table: + select_and_search: "Velg filtre og klikk på SØK for å få tilgang til dataene dine." bulk_coop: bulk_coop_supplier_report: 'Bulk Co-op - Totaler etter Leverandør' bulk_coop_allocation: 'Bulk Co-op - Allokering' bulk_coop_packing_sheets: 'Bulk Co-op - Pakkseddel' bulk_coop_customer_payments: 'Bulk Co-op - Kunde Betalinger' + users: + email_confirmation: + confirmation_pending: "Epostbekreftelse venter. Vi har sendt en bekreftelses-epost til %{address}." variants: autocomplete: producer_name: Produsent + general_settings: + edit: + legal_settings: "Juridiske innstillinger" + cookies_consent_banner_toggle: "Vis informasjonskapsler samtykkebanner" + privacy_policy_url: "Personvernspolicy URL" + enterprises_require_tos: "Bedrifter må godta Tjenestevilkår" + cookies_policy_matomo_section: "Vis Matomo-delen på informasjonskapsler-siden" + cookies_policy_ga_section: "Vis Google Analytics-delen om informasjonskapsler på policy-siden om informasjonskapsler" + footer_tos_url: "Vilkår URL" checkout: payment: stripe: @@ -2560,5 +2641,9 @@ nb: saved_cards: default?: Standard? delete?: Slett? + cards: + authorised_shops: Autoriserte Butikker + authorised_shops_popover: Dette er listen over butikker som har lov til å belaste ditt standard kredittkort for eventuelle abonnementer (dvs. gjentatte ordre) du måtte ha. Kortinformasjonen din vil bli holdt sikker og vil ikke bli delt med butikkeiere. Du vil alltid bli varslet når du blir belastet. + saved_cards_popover: Dette er listen over kort du har valgt å lagre for senere bruk. Din standard vil bli valgt automatisk når du gjør en bestilling, og kan belastes av butikker du har gitt lov til å gjøre det (se høyre). localized_number: invalid_format: har et ugyldig format. Vennligst skriv inn et nummer. diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 9dea0d380d..cd8c683880 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -18,8 +18,6 @@ pt: attributes: email: taken: "Já existe uma conta associada a este email. Por favor faça login ou defina uma nova palavra-passe." - spree/order: - no_card: Não há cartões de crédito válidos disponíveis order_cycle: attributes: orders_close_at: @@ -43,9 +41,6 @@ pt: invalid_type: "tem de ser em dinheiro ou método Stripe" shipping_method: not_available_to_shop: "não está disponível para %{shop}" - credit_card: - not_available: "não está disponível" - blank: "é obrigatório" devise: confirmations: send_instructions: "Daqui a uns minutos irá receber um email com instruções sobre como confirmar a sua conta." @@ -160,6 +155,7 @@ pt: distributors: Distribuidores distribution: Distribuição bulk_order_management: Gestão de Encomendas por Atacado + enterprises: Organizações enterprise_groups: Grupos reports: Relatórios variant_overrides: Inventário @@ -310,6 +306,35 @@ pt: included_tax_tip: "A taxa total incluída no exemplo de conta mensal, dadas as definições e o volume de negócios indicado." total_monthly_bill_incl_tax: "Total da Conta Mensal (taxa incluída)" total_monthly_bill_incl_tax_tip: "O exemplo de conta mensal total incluindo taxas, dadas as definições e o volume de negócios indicado." + cache_settings: + show: + title: A carregar + distributor: Distribuidor + order_cycle: Ciclo de Encomendas + status: Status + diff: Diff + error: Erro + invoice_settings: + edit: + title: Configuração de Faturas + invoice_style2?: Use o modelo alternativo de fatura que inclui o total de impostos dividido por taxa e taxa de imposto por item (ainda não disponível para países que exibem preços sem taxas) + enable_receipt_printing?: Mostrar opções para imprimir recibos usando impressoras térmicas no selector de encomendas? + stripe_connect_settings: + edit: + title: "Ligar ao Stripe" + settings: "Definições" + stripe_connect_enabled: Permitir às lojas que aceitem pagamentos usando a ligação ao Stripe? + no_api_key_msg: Não existem contas Stripe associadas a esta organização. + configuration_explanation_html: Para instruções detalhadas sobre como configurar a integração com Stripe Connect, por favor consulte este guia. + status: Estado + ok: Ok + instance_secret_key: Chave Secreta da Instância + account_id: ID de Conta + business_name: Nome do Negócio + charges_enabled: Taxas activas + charges_enabled_warning: "Aviso: As taxas não estão activas para a sua conta" + auth_fail_error: A chave da API que indicou não é válida + empty_api_key_error_html: Não foi fornecida nenhuma chave de API Stripe. Para definir a sua chave de API, por favor siga estas instruções customers: index: add_customer: "Adicionar Consumidor/a" @@ -334,14 +359,6 @@ pt: search_by_email: "Pesquisar por e-mail/código" destroy: has_associated_orders: 'Não foi possível apagar: o/a consumidor/a tem encomendas associadas a esta loja.' - cache_settings: - show: - title: A carregar - distributor: Distribuidor - order_cycle: Ciclo de Encomendas - status: Status - diff: Diff - error: Erro contents: edit: title: Conteúdo @@ -414,20 +431,18 @@ pt: index: select_file: Selecione uma folha de cálculo para carregar spreadsheet: Folha de cálculo - import_into: "Importar para:" product_list: Lista de produtos inventories: Inventários import: Importar upload: Carregar import: review: Rever - proceed: Continuar + import: Importar save: Guardar results: Resultados save_imported: Guardar produtos importados no_valid_entries: Não foram encontradas entradas válidas none_to_save: Não foram encontradas entradas que possam ser guardadas - some_invalid_entries: Ficheiro importado contém algumas entradas inválidas save_valid?: Guardar entradas válidas e descartar as outras? no_errors: Nenhum erro detectado. save_all_imported?: Guardar todos os produtos importados? @@ -436,7 +451,6 @@ pt: not_found: organização não encontrada no_name: Sem nome blank_supplier: alguns produtos têm o nome do fornecedor vazio - reset_absent?: Restabelecer produtos ausentes? overwrite_all: Substituir todos overwrite_empty: Substituir se vazio default_stock: Definir nível de stock @@ -454,7 +468,7 @@ pt: inventory_to_reset: Itens de inventário existentes terão o nível de stock restabelecido a zero line: Linha item_line: Linha de item - save: + save_results: final_results: Importar resultados finais products_created: Produtos criados products_updated: Produtos actualizados @@ -463,6 +477,8 @@ pt: products_reset: Os produtos tiveram o nível de stock restabelecido a zero inventory_reset: Itens de inventário tiveram o nível de stock restabelecido a zero all_saved: "Todos os itens guardados com sucesso" + some_saved: "itens guardados com sucesso" + save_errors: Erros a guardar variant_overrides: loading_flash: loading_inventory: A CARREGAR INVENTÁRIO... @@ -681,7 +697,7 @@ pt: email_confirmed: "Email confirmado" email_not_confirmed: "Email não confirmado" actions: - edit_profile: Editar Perfil + edit_profile: Definições properties: Propriedades payment_methods: Métodos de pagamento payment_methods_tip: Esta organização não tem formas de pagamento definidas @@ -721,13 +737,13 @@ pt: no_enterprises_found: Nenhuma organização encontrada. search_placeholder: Procurar por nome manage: Gerir + manage_link: Definições new_form: owner: Proprietário owner_tip: O utilizador principal responsável por esta organização. i_am_producer: Sou um produtor contact_name: Nome do contacto edit: - editing: 'Edição:' back_link: Voltar à lista de organizações new: title: Nova Organização @@ -813,6 +829,9 @@ pt: schedule_present: Esse ciclo de encomendas está ligado a um horário e não pode ser apagado. Por favor elimine a ligação ou apague primeiro o horário. bulk_update: no_data: Hmmm, algo correu mal. Não foram encontrados dados do ciclo de encomendas. + date_warning: + cancel: Cancelar + proceed: Continuar producer_properties: index: title: Propriedades do Produtor @@ -824,11 +843,6 @@ pt: shared: user_guide_link: user_guide: Manual do Utilizador - invoice_settings: - edit: - title: Configuração de Faturas - invoice_style2?: Use o modelo alternativo de fatura que inclui o total de impostos dividido por taxa e taxa de imposto por item (ainda não disponível para países que exibem preços sem taxas) - enable_receipt_printing?: Mostrar opções para imprimir recibos usando impressoras térmicas no selector de encomendas? overview: enterprises_header: ofn_with_tip: As Organizações são Produtores e/ou Hubs e representam a unidade básica de organização dentro da Open Food Network. @@ -927,7 +941,6 @@ pt: invalid_error: Ooops! Por favor preencha todos os campos obrigatórios... allowed_payment_method_types_tip: De momento, só podem ser usados métodos de pagamento em Dinheiro ou Stripe credit_card: Cartão de Crédito - no_cards_available: Não existem cartões disponíveis loading_flash: loading: A CARREGAR SUBSCRIÇÕES review: @@ -957,22 +970,6 @@ pt: schedules: destroy: associated_subscriptions_error: Este horário não pode ser eliminado porque tem subscrições associadas - stripe_connect_settings: - edit: - title: "Ligar ao Stripe" - settings: "Definições" - stripe_connect_enabled: Permitir às lojas que aceitem pagamentos usando a ligação ao Stripe? - no_api_key_msg: Não existem contas Stripe associadas a esta organização. - configuration_explanation_html: Para instruções detalhadas sobre como configurar a integração com Stripe Connect, por favor consulte este guia. - status: Estado - ok: Ok - instance_secret_key: Chave Secreta da Instância - account_id: ID de Conta - business_name: Nome do Negócio - charges_enabled: Taxas activas - charges_enabled_warning: "Aviso: As taxas não estão activas para a sua conta" - auth_fail_error: A chave da API que indicou não é válida - empty_api_key_error_html: Não foi fornecida nenhuma chave de API Stripe. Para definir a sua chave de API, por favor siga estas instruções controllers: enterprises: stripe_connect_cancelled: "A ligação ao Stripe foi cancelada" @@ -997,6 +994,29 @@ pt: register_call: selling_on_ofn: "Tem interesse em participar na Open Food Network?" register: "Registe-se aqui" + footer: + footer_global_headline: "OFN Global" + footer_global_home: "Início" + footer_global_news: "Notícias" + footer_global_about: "Sobre" + footer_global_contact: "Contacto" + footer_sites_headline: "Páginas OFN" + footer_sites_developer: "Desenvolvimento" + footer_sites_community: "Comunidade" + footer_sites_userguide: "Manual do Utilizador" + footer_secure: "Seguro e de confiança." + footer_secure_text: "A Open Food Network utiliza a criptografia SSL (2048 bit RSA) para manter as suas informações em segurança. Os nossos servidores não guardam os detalhes do seu cartão de crédito e os pagamentos são processados por serviços compatíveis com PCI." + footer_contact_headline: "Ficamos em contacto" + footer_contact_email: "Envie-nos um email" + footer_nav_headline: "Navegar" + footer_join_headline: "Junte-se a nós" + footer_join_body: "Crie uma lista de ofertas, uma loja ou um grupo de consumo na Open Food Network" + footer_join_cta: "Quero saber mais!" + footer_legal_call: "Leia os nossos" + footer_legal_tos: "Termos e condições" + footer_legal_visit: "Encontre-nos no" + footer_legal_text_html: "A Open Food Network é uma plataforma livre e de código aberto. O nosso conteúdo tem uma licença %{content_license} e o nosso código %{code_license}." + footer_skylight_dashboard_html: Dados de performance disponível em %{dashboard}. shop: messages: login: "Entrar" @@ -1005,6 +1025,7 @@ pt: require_customer_login: "Essa loja é somente para clientes." require_login_html: "Por favor %{login} se já tem uma conta. Caso contrário, %{register} para se tornar consumidor." require_customer_html: "Por favor %{contact} a %{enterprise} para se tornar consumidor/a. " + card_could_not_be_updated: O cartão não pode ser actualizado card_could_not_be_saved: o cartão não pode ser guardado spree_gateway_error_flash_for_checkout: "Houve um problema com a sua informação de pagamento: %{error}" invoice_billing_address: "Morada de faturação:" @@ -1031,6 +1052,14 @@ pt: ticket_column_item: "Item" ticket_column_unit_price: "Preço Unitário" ticket_column_total_price: "Preço Total" + menu_1_title: "Lojas" + menu_1_url: "/shops" + menu_2_title: "Mapa" + menu_3_title: "Produtores" + menu_4_title: "Grupos" + menu_5_title: "Sobre" + menu_6_title: "Conectar" + menu_7_title: "Aprender" logo: "Logo (640x130)" logo_mobile: "logo mobile (75x26)" logo_mobile_svg: "Logo mobile (svg)" @@ -1046,7 +1075,6 @@ pt: footer_email: "Email" footer_links_md: "Links" footer_about_url: "URL Sobre" - footer_tos_url: "URL dos Termos de Serviço" name: Nome first_name: Primeiro Nome last_name: Último Nome @@ -1120,27 +1148,6 @@ pt: ie_warning_firefox: Descarregar Firefox ie_warning_ie: Actualizar Internet Explorer ie_warning_other: "Não consegue actualizar o navegador? Tente aceder à OFN pelo smartphone :-)" - footer_global_headline: "OFN Global" - footer_global_home: "Início" - footer_global_news: "Notícias" - footer_global_about: "Sobre" - footer_global_contact: "Contacto" - footer_sites_headline: "Páginas OFN" - footer_sites_developer: "Desenvolvimento" - footer_sites_community: "Comunidade" - footer_sites_userguide: "Manual do Utilizador" - footer_secure: "Seguro e de confiança." - footer_secure_text: "A Open Food Network utiliza a criptografia SSL (2048 bit RSA) para manter as suas informações em segurança. Os nossos servidores não guardam os detalhes do seu cartão de crédito e os pagamentos são processados por serviços compatíveis com PCI." - footer_contact_headline: "Ficamos em contacto" - footer_contact_email: "Envie-nos um email" - footer_nav_headline: "Navegar" - footer_join_headline: "Junte-se a nós" - footer_join_body: "Crie uma lista de ofertas, uma loja ou um grupo de consumo na Open Food Network" - footer_join_cta: "Quero saber mais!" - footer_legal_call: "Leia os nossos" - footer_legal_tos: "Termos e condições" - footer_legal_visit: "Encontre-nos no" - footer_legal_text_html: "A Open Food Network é uma plataforma livre e de código aberto. O nosso conteúdo tem uma licença %{content_license} e o nosso código %{code_license}." home_shop: Ir às compras brandstory_headline: "Para quem consome com princípios" brandstory_intro: "Às vezes a melhor forma de consertar o sistema é construir um novo..." @@ -1552,6 +1559,17 @@ pt: reset_password: "Redefinir palavra-passe" who_is_managing_enterprise: "Quem é responsável por gerir %{enterprise}? " update_and_recalculate_fees: "Actualizar e Recalcular Taxas" + registration: + steps: + type: + headline: "Último passo para adicionar %{enterprise}!" + question: "É produtor/a?" + yes_producer: "Sim, sou produtor/a" + no_producer: "Não, não sou produtor/a" + producer_field_error: "Por favor escolha uma opção. É produtor/a?" + yes_producer_help: "Produtores/as são quem faz coisas deliciosas para comer e/ou beber. É produtor/a se planta, cria, fermenta, amassa, munge ou molda algo." + no_producer_help: "Se não é produtor/a, é provavelmente alguém que vende e distribui alimentos. Pode ser uma cooperativa, um grupo de consumo, um distribuidor, um retalhista, ou outro." + create_profile: "Criar perfil" enterprise: registration: modal: @@ -1590,13 +1608,6 @@ pt: phone_field_placeholder: 'ex: 97 1234 5678' type: title: 'Tipo' - headline: "Último passo para adicionar %{enterprise}!" - question: "É produtor/a?" - yes_producer: "Sim, sou produtor/a" - no_producer: "Não, não sou produtor/a" - producer_field_error: "Por favor escolha uma opção. É produtor/a?" - yes_producer_help: "Produtores/as são quem faz coisas deliciosas para comer e/ou beber. É produtor/a se planta, cria, fermenta, amassa, munge ou molda algo." - no_producer_help: "Se não é produtor/a, é provavelmente alguém que vende e distribui alimentos. Pode ser uma cooperativa, um grupo de consumo, um distribuidor, um retalhista, ou outro." about: title: 'Sobre' images: @@ -1684,7 +1695,6 @@ pt: registration_type_error: "Por favor escolha uma opção. É produtor/a?" registration_type_producer_help: "Produtores/as são quem faz coisas deliciosas para comer e/ou beber. É produtor/a se planta, cria, fermenta, amassa, munge ou molda algo." registration_type_no_producer_help: "Se não é produtor/a, é provavelmente alguém que vende e distribui alimentos. Pode ser uma cooperativa, um grupo de consumo, um distribuidor, um retalhista, ou outro." - create_profile: "Criar perfil" registration_images_headline: "Obrigado!" registration_images_description: "Vamos adicionar umas boas imagens para o seu perfil ficar impecável!" registration_detail_headline: "Vamos Começar" @@ -1743,7 +1753,6 @@ pt: you_have_no_orders_yet: "Ainda não tem encomendas" running_balance: "Saldo corrente" outstanding_balance: "Saldo pendente" - admin_entreprise_relationships: "Relações da Organização" admin_entreprise_relationships_everything: "Tudo" admin_entreprise_relationships_permits: "permite" admin_entreprise_relationships_seach_placeholder: "Procurar" @@ -1867,11 +1876,7 @@ pt: edit_profile_details_etc: "Modificar o seu perfil: descrição, imagem, etc." order_cycle: "Ciclo de Encomendas" order_cycles: "Ciclos de Encomendas" - enterprises: "Organizações" - enterprise_relationships: "Relações da Organização" remove_tax: "Remover imposto" - enterprise_terms_of_service: "Termos de Serviço da Organização" - enterprises_require_tos: "As organizações têm de aceitar os Termos de Serviço" enterprise_tos_link: "Ligação para Termos de Serviço da Organização" enterprise_tos_message: "Queremos trabalhar com pessoas que partilham os nossos objectivos e valores. Por isso pedimos às organizações novas que concordem com os nossos" enterprise_tos_link_text: "Termos de Serviço." @@ -2126,6 +2131,7 @@ pt: order_cycles_no_permission_to_coordinate_error: "Nenhuma das suas organizações tem permissão para coordenar um ciclo de encomendas." order_cycles_no_permission_to_create_error: "Não tem permissão para criar um ciclo de encomendas coordenado por essa organização." back_to_orders_list: "Voltar à lista de encomendas" + no_orders_found: "Nenhuma encomenda encontrada" order_information: "Informação da Encomenda" date_completed: "Data de Conclusão" amount: "Quantia" @@ -2150,6 +2156,7 @@ pt: choose: Escolha resolve_errors: Por favor resolva os seguintes erros more_items: "+ %{count} Mais" + default_card_updated: Cartão por defeito actualizado admin: enterprise_limit_reached: "Atingiu o número limite de organizações por conta. Escreva para %{contact_email} se precisar de aumentá-lo." modals: @@ -2271,6 +2278,8 @@ pt: resolve: Resolver new_tag_rule_dialog: select_rule_type: "Selecionar um tipo de regra:" + resend_user_email_confirmation: + resend: "Reenviar" out_of_stock: reduced_stock_available: Stock reduzido disponível out_of_stock_text: > @@ -2370,10 +2379,6 @@ pt: account_id: ID de Conta business_name: Nome do Negócio charges_enabled: Taxas activas - payments: - source_forms: - stripe: - no_payment_via_admin_backend: Neste momento não é possível criar pagamentos baseados em Stripe a partir do painel de administrador products: new: title: 'Novo Produto' @@ -2394,6 +2399,7 @@ pt: inherits_properties?: Herda Propriedades? available_on: Disponível em av_on: "Disp. em" + import_date: "Data de Importação" products_variant: variant_has_n_overrides: "Esta variante tem %{n}substituição(ões)" new_variant: "Nova variante" @@ -2411,12 +2417,13 @@ pt: bulk_coop_allocation: 'Cooperativa por Atacado - Alocação' bulk_coop_packing_sheets: 'Cooperativa por Atacado - Folhas de Empacotamento' bulk_coop_customer_payments: 'Cooperativa por Atacado - Pagamentos do Consumidor' - shared: - configuration_menu: - stripe_connect: Ligar ao Stripe variants: autocomplete: producer_name: Produtor + general_settings: + edit: + enterprises_require_tos: "As organizações têm de aceitar os Termos de Serviço" + footer_tos_url: "URL dos Termos de Serviço" checkout: payment: stripe: @@ -2521,6 +2528,7 @@ pt: paid?: Pago? view: Ver saved_cards: + default?: Por defeito? delete?: Apagar? localized_number: invalid_format: tem um formato inválido. Por favor introduza um número. diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 6b8ff19728..f5072de5f2 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -69,6 +69,7 @@ sv: distributors: Distributörer distribution: Distribution bulk_order_management: Hantera flera beställningar + enterprises: Företag enterprise_groups: Grupper reports: Rapporter variant_overrides: Lager @@ -123,6 +124,8 @@ sv: form_invalid: "Formuläret saknar data eller har innehåller ogiltiga fält" clear_filters: Rensa filterinställningar clear: Rensa + cancel: Avbryt + back: Backa columns: Kolumner actions: Handlingar viewing: "Tittar på: %{current_view_name}" @@ -186,6 +189,19 @@ sv: included_tax_tip: "All skatt är inkluderad i exemplets månadsfaktura enligt de inställningar och den omsättning som var angiven." total_monthly_bill_incl_tax: "Totalt månadsfaktura (inkl moms)" total_monthly_bill_incl_tax_tip: "Exemplets totala månadsfaktura med skatt inkluderad enligt de inställningar och den omsättning som var angiven." + cache_settings: + show: + title: Inkomst + distributor: Distributör + order_cycle: Beställningsomgång + status: Status + diff: Differens + error: Fel + invoice_settings: + edit: + title: Fakturainställningar + invoice_style2?: Använd den alternativa faktureringsmodellen som inkluderar totala skattefördelning per beräkning och skatt per artikel (ännu inte passande för länder som visar priser exklusive skatt) + enable_receipt_printing?: 'Visa alternativ för att skriva ut recept genom att använda termiska skrivare i ordermenyn? ' customers: index: add_customer: "Lägg till kund" @@ -210,14 +226,6 @@ sv: search_by_email: "Sök via e-post/kod..." destroy: has_associated_orders: 'Radering misslyckades: kunden har associerade order med sin butik' - cache_settings: - show: - title: Inkomst - distributor: Distributör - order_cycle: Beställningsomgång - status: Status - diff: Differens - error: Fel contents: edit: title: Innehåll @@ -247,12 +255,28 @@ sv: manages: hanterar products: unit_name_placeholder: 't.ex. klasar' + index: + unit: Enhet + display_as: Visa som + category: Kategori + tax_category: Skattekategori + inherits_properties?: Ärva egenskaper? + available_on: 'Tillgänglig ' + av_on: "Md. På" Search: Sök properties: property_name: Egendomens Namn inherited_property: Ärvd Egendom variants: to_order_tip: "Varor som framställs för en order har inget eget varunummer, ex formbröd som värms vid en order." + product_import: + file_not_found: Filen hittades inte eller kunde inte öppnas + no_data: Ingen data hittades i kalkylbladet + model: + no_file: "Fel: ingen fil laddades upp" + could_not_process: "kunde inte hantera filen: ogiltig filtyp" + blank: kan inte vara blank + none_saved: sparade inte några produkter variant_overrides: loading_flash: loading_inventory: LADDNING AV INVENTARIELISTA @@ -442,7 +466,7 @@ sv: managers: Chefer managers_tip: Andra användare med tillstånd att leda detta företag. actions: - edit_profile: Redigera profilen + edit_profile: Inställningar properties: Egenskaper payment_methods: Betalningssätt payment_methods_tip: Detta företag har inga betalningssätt @@ -482,13 +506,13 @@ sv: no_enterprises_found: Inget företag kunde hittas. search_placeholder: Sök på namn manage: Administrera + manage_link: Inställningar new_form: owner: Ägare owner_tip: Den primära användaren är ansvarig för detta företag. i_am_producer: Jag är en Tillverkare contact_name: Kontaktperson edit: - editing: 'Redigera:' back_link: Åter till företagslistan new: title: Nya företag @@ -497,7 +521,6 @@ sv: welcome_title: Välkommen till OFN! welcome_text: Du har korrekt slutfört en next_step: Nästa steg - choose_starting_point: 'Välj din startpunkt:' order_cycles: edit: advanced_settings: Avancerade inställningar @@ -530,7 +553,7 @@ sv: name: Namn orders_open: Order öppna till coordinator: Koordinator - order_closes: Order stänger + orders_close: Order stänger row: suppliers: leverantörer distributors: distributörer @@ -542,17 +565,14 @@ sv: customer_instructions_placeholder: Hämtnings eller leverans meddelanden products: Produkter fees: Avgifter + date_warning: + cancel: Avbryt producer_properties: index: title: Producentegenskaper shared: user_guide_link: user_guide: Användarinstruktion - invoice_settings: - edit: - title: Fakturainställningar - invoice_style2?: Använd den alternativa faktureringsmodellen som inkluderar totala skattefördelning per beräkning och skatt per artikel (ännu inte passande för länder som visar priser exklusive skatt) - enable_receipt_printing?: 'Visa alternativ för att skriva ut recept genom att använda termiska skrivare i ordermenyn? ' overview: enterprises_header: ofn_with_tip: Företag är Producenter och/eller Hubbar och är den grundläggande organisationsenheten inom Open Food Network. @@ -638,6 +658,28 @@ sv: register_call: selling_on_ofn: "Intresserad av att gå med i Open Food Network?" register: "Registrera dig här" + footer: + footer_global_headline: "OFN Globalt" + footer_global_home: "Hem" + footer_global_news: "Nyheter" + footer_global_about: "Om oss" + footer_global_contact: "Kontakt" + footer_sites_headline: "OFN webbsidor" + footer_sites_developer: "Utvecklare" + footer_sites_community: "Gemenskap" + footer_sites_userguide: "Användarinstruktion" + footer_secure: "Säkert och anförtrott." + footer_secure_text: "Open Food Network använder SSL-kryptering (2048 bitars RSA) överallt för att hålla din beställnings- och betalningsinformation privat. Våra servrar lagrar inte dina kreditkortsdetaljer och betalningar processeras av PCI-beskedliga tjänster." + footer_contact_headline: "Håll kontakten" + footer_contact_email: "Mejla oss" + footer_nav_headline: "Navigera" + footer_join_headline: "Gå med oss" + footer_join_body: "Skapa en lista, butik eller gruppförteckning på Open Food Network." + footer_join_cta: "Berätta mer!" + footer_legal_call: "Läs vår" + footer_legal_tos: "Användarvillkor" + footer_legal_visit: "Hitta oss på" + footer_legal_text_html: "Open Food Network är en avgiftsfri plattform för öppen källkod. Vårt innehåll har licens med %{content_license} och vår kod med %{code_license}." shop: messages: login: "Logga in" @@ -670,6 +712,13 @@ sv: ticket_column_item: "Vara" ticket_column_unit_price: "Styckpris" ticket_column_total_price: "Summa" + menu_1_title: "Butiker" + menu_2_title: "Karta" + menu_3_title: "Producenter " + menu_4_title: "Grupper" + menu_5_title: "Om oss" + menu_6_title: "Anslut" + menu_7_title: "Lär mer" logo: "Logotyp (640x130)" logo_mobile: "Mobil logotyp (75x26)" logo_mobile_svg: "Mobil logotyp (SVG)" @@ -685,7 +734,6 @@ sv: footer_email: "epost" footer_links_md: "Webblänkar " footer_about_url: "Om oss URL" - footer_tos_url: "Användarvillkor URL" name: Namn first_name: 'Förnamn ' last_name: 'Efternamn ' @@ -741,27 +789,6 @@ sv: ie_warning_firefox: Hämta Firefox ie_warning_ie: Uppdatera din Internet Explorer webbläsare ie_warning_other: "Kan du inte uppdatera din webbläsare? Pröva Open Food Network på din smartphone :-)" - footer_global_headline: "OFN Globalt" - footer_global_home: "Hem" - footer_global_news: "Nyheter" - footer_global_about: "Om oss" - footer_global_contact: "Kontakt" - footer_sites_headline: "OFN webbsidor" - footer_sites_developer: "Utvecklare" - footer_sites_community: "Gemenskap" - footer_sites_userguide: "Användarguide" - footer_secure: "Säkert och anförtrott." - footer_secure_text: "Open Food Network använder SSL-kryptering (2048 bitars RSA) överallt för att hålla din beställnings- och betalningsinformation privat. Våra servrar lagrar inte dina kreditkortsdetaljer och betalningar processeras av PCI-beskedliga tjänster." - footer_contact_headline: "Håll kontakten" - footer_contact_email: "Mejla oss" - footer_nav_headline: "Navigera" - footer_join_headline: "Gå med oss" - footer_join_body: "Skapa en lista, butik eller gruppförteckning på Open Food Network." - footer_join_cta: "Berätta mer!" - footer_legal_call: "Läs vår" - footer_legal_tos: "Användarvillkor" - footer_legal_visit: "Hitta oss på" - footer_legal_text_html: "Open Food Network är en avgiftsfri plattform för öppen källkod. Vårt innehåll har licens med %{content_license} och vår kod med %{code_license}." home_shop: Handla nu brandstory_headline: "Mat, icke-inkorporerad." brandstory_intro: "Ibland är det bästa sättet att fixa till systemet att starta ett nytt..." @@ -1142,6 +1169,17 @@ sv: password_reset_sent: "Ett e-postmeddelande med instruktioner hur lösenordet återställs har översänts!" reset_password: "Återställ lösenordet" who_is_managing_enterprise: "Vem är ansvarig hos %{enterprise}?" + registration: + steps: + type: + headline: "Sista steget för att lägga till %{enterprise}!" + question: "Är du en producent?" + yes_producer: "Ja, jag är en procucdent" + no_producer: "Nej, jag är inte en producent" + producer_field_error: "Var vänlig välj en. Är du en producent?" + yes_producer_help: "Producenter gör smakfulla rätter att äta och/eller dricka. Du är en producent om du odlar det, föder upp det, brygger det, bakar det behandlar det, förädlar det eller formar det." + no_producer_help: "Om du inte ät en producent är du antagligen någon som säljer och distribuerar mat. Du borde bli ett matställe, kooperation, inköpsgrupp, detaljhandlare,grossist eller annat. " + create_profile: "Skapa en profil" enterprise_contact: "Kontaktperson" enterprise_contact_required: "Du måste ange en kontaktperson." enterprise_email_address: "E-postadress" @@ -1219,7 +1257,6 @@ sv: registration_type_error: "Var vänlig välj en. Är du en producent?" registration_type_producer_help: "Producenter gör smakfulla rätter att äta och/eller dricka. Du är en producent om du odlar det, föder upp det, brygger det, bakar det behandlar det, förädlar det eller formar det." registration_type_no_producer_help: "Om du inte ät en producent är du antagligen någon som säljer och distribuerar mat. Du borde bli ett matställe, kooperation, inköpsgrupp, detaljhandlare,grossist eller annat. " - create_profile: "Skapa en profil" registration_images_headline: "Tack!" registration_images_description: "Låt oss ladda några säljande bilder så att din profil ser lockande ut! :)" registration_detail_headline: "Låt oss börja" @@ -1278,7 +1315,6 @@ sv: you_have_no_orders_yet: "Du har inga order ännu" running_balance: "Saldo" outstanding_balance: "Utestående behållning" - admin_entreprise_relationships: "Företagssamband" admin_entreprise_relationships_everything: "Allting" admin_entreprise_relationships_permits: "tillåtelse" admin_entreprise_relationships_seach_placeholder: "Sök" @@ -1352,12 +1388,7 @@ sv: spree_admin_enterprises_fees: "Företagsavgifter" spree_admin_enterprises_none_create_a_new_enterprise: "SKAPA ETT NYTT FÖRETAG" spree_admin_enterprises_none_text: "Du har inget företag ännu" - spree_admin_enterprises_producers_name: "Namn" - spree_admin_enterprises_producers_total_products: "Summa produkter" - spree_admin_enterprises_producers_active_products: "Aktiva produkter" - spree_admin_enterprises_producers_order_cycles: "Produkter i beställningsrunda" spree_admin_enterprises_tabs_hubs: "MATSTÄLLEN" - spree_admin_enterprises_tabs_producers: "PRODUCENTER" spree_admin_enterprises_producers_manage_products: "HANTERA PRODUKTER" spree_admin_enterprises_any_active_products_text: "Du har inga aktiva produkter." spree_admin_enterprises_create_new_product: "SKAPA EN NY PRODUKT" @@ -1396,10 +1427,7 @@ sv: edit_profile_details_etc: "Ändra beskrivningen av din profil, bilder, etc" order_cycle: "Ordercykel" order_cycles: "Beställningsomgångar" - enterprises: "Företag" remove_tax: "Tag bort skatt" - enterprise_terms_of_service: "Företagets servicevillkor" - enterprises_require_tos: "Företag måste acceptera servicevillkoren" enterprise_tos_link: "Länk till företagets servicevillkor" enterprise_tos_message: "Vi vill arbeta med personer som delar våra mål och värderingar. Därför ber vi nya företag att samtycka med vår" enterprise_tos_link_text: "Servicevillkor" @@ -1616,7 +1644,6 @@ sv: content_configuration_pricing_table: "(TODO: Pristabell)" content_configuration_case_studies: "(TODO: Case studies)" content_configuration_detail: "(TODO: Detalj)" - enterprise_name_error: "är redan registrerad. Om det här är ditt företag och du vill göra anspråk på den, vänligen kontakta nuvarande administratör för den här profilen på %{email}." enterprise_owner_error: "^ %{email} har inte rätt att äga några fler företag (gränsen är %{enterprise_limit})." enterprise_role_uniqueness_error: "^ Den rollen existerar redan." inventory_item_visibility_error: Måste vara 'true' eller 'false' @@ -1778,6 +1805,8 @@ sv: resolve: Besluta new_tag_rule_dialog: select_rule_type: "Välj en regeltyp:" + resend_user_email_confirmation: + resend: "Återsänd" out_of_stock: reduced_stock_available: Minska tillgängligt lager out_of_stock_text: > @@ -1862,6 +1891,15 @@ sv: title: LADDAR PRODUKTER no_products: "Inga produkter ännu. Varför lägger du inte till några?" no_results: "Ledsen, inga resultat stämmer överens" + products_head: + name: Namn + unit: Enhet + display_as: Visa som + category: Kategori + tax_category: Skattekategori + inherits_properties?: Ärva egenskaper? + available_on: 'Tillgänglig ' + av_on: "Md. På" reports: bulk_coop: bulk_coop_supplier_report: 'Bulk Co-op - Totaler per leverantör' @@ -1871,6 +1909,10 @@ sv: variants: autocomplete: producer_name: Producent + general_settings: + edit: + enterprises_require_tos: "Företag måste acceptera servicevillkoren" + footer_tos_url: "Användarvillkor URL" date_picker: format: '%Y-%m-%d' js_format: 'åå-mm-dd' From 6e55e5b1c7039fbc3e00d02ea738f0705916a0a7 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Wed, 15 Aug 2018 03:03:03 +0800 Subject: [PATCH 028/190] Use maximum of 80% for modals in large screens When there is enough content in the modal, the height of the modal plus its top margin could exceed the height of the viewport. Considering a top position of 10%, a max height of 80% renders a tall modal vertically centered, with 10% remaining space at the bottom. --- app/assets/stylesheets/darkswarm/modals.css.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/modals.css.scss b/app/assets/stylesheets/darkswarm/modals.css.scss index 538a5622ed..300e769f01 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.scss +++ b/app/assets/stylesheets/darkswarm/modals.css.scss @@ -29,7 +29,7 @@ dialog // Medium and up - when modal IS NOT full screen @media only screen and (min-width: 641px) { top: 10%; - max-height: 100%; + max-height: 80%; } } From 34adf7cf6c4722e39660b24de5b3a5662583d0f0 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Wed, 15 Aug 2018 03:07:14 +0800 Subject: [PATCH 029/190] Lower the start point for sliding of modals Occasionally, the page scrolls up while the modal is being opened. This was causing the final position of the modal to be at the wrong location relative to the viewport. This was happening because of a race condition between the animation that slides the modal from above the viewport to the middle, and focus() which the modal does: https://github.com/yalabot/angular-foundation/blob/0.8.0/src/modal/modal.js#L109 The final vertical position of the modal is at 10%, so the animation which translates the modal -25% vertically was starting -15% above the viewport. The focus() was then causing vertical scroll. This lowers the starting point of the animation, so there will no longer be scrolling. Additionally, the animation would only happen on large screens. The CSS property "top" is 0 for smaller screens. --- app/assets/stylesheets/darkswarm/animations.scss | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/animations.scss b/app/assets/stylesheets/darkswarm/animations.scss index 7601da02dc..8a6efdc161 100644 --- a/app/assets/stylesheets/darkswarm/animations.scss +++ b/app/assets/stylesheets/darkswarm/animations.scss @@ -123,9 +123,12 @@ -moz-transition: -moz-transform 0.2s ease-out; -o-transition: -o-transform 0.2s ease-out; transition: transform 0.2s ease-out; - -webkit-transform: translate(0, -25%); - -ms-transform: translate(0, -25%); - transform: translate(0, -25%); + + @media only screen and (min-width: 641px) { + -webkit-transform: translate(0, -10%); + -ms-transform: translate(0, -10%); + transform: translate(0, -10%); + } } .reveal-modal.in { From a37e9f1b87be78021dcd2379d71d4d4384f2b24d Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Wed, 15 Aug 2018 19:26:28 +0800 Subject: [PATCH 030/190] Use v0.9.0-20180826174721 in kristinalim fork of angular-foundation There is a bug in the handling of % values for the "top" CSS property of the modals. See details here: https://github.com/kristinalim/angular-foundation/pull/1 A PR to the original repository has also been submitted, but the project doesn't seem to be active anymore: https://github.com/yalabot/angular-foundation/pull/319 And to another fork of the repository: https://github.com/cwadrupldijjit/angular-foundation/pull/1 The bug was causing the 10% "top" CSS property for the modal to be treated as 10px. --- app/assets/javascripts/admin/all.js | 2 +- app/assets/javascripts/darkswarm/all.js.coffee | 2 +- .../javascripts/shared/mm-foundation-tpls-0.8.0.min.js | 10 ---------- .../mm-foundation-tpls-0.9.0-20180826174721.min.js | 10 ++++++++++ spec/javascripts/application_spec.js | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 app/assets/javascripts/shared/mm-foundation-tpls-0.8.0.min.js create mode 100644 app/assets/javascripts/shared/mm-foundation-tpls-0.9.0-20180826174721.min.js diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 292b5e21ef..eddb8a0dfb 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -61,7 +61,7 @@ //= require moment/nb.js //= require moment/pt-br.js //= require moment/sv.js -//= require ../shared/mm-foundation-tpls-0.8.0.min.js +//= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js //= require angularjs-file-upload //= require_tree . diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 90080f0a77..f781481627 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -11,7 +11,7 @@ #= require lodash.underscore.js #= require angular-scroll.min.js #= require angular-google-maps.min.js -#= require ../shared/mm-foundation-tpls-0.8.0.min.js +#= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js #= require ../shared/ng-infinite-scroll.min.js #= require ../shared/angular-local-storage.js #= require ../shared/angular-slideables.js diff --git a/app/assets/javascripts/shared/mm-foundation-tpls-0.8.0.min.js b/app/assets/javascripts/shared/mm-foundation-tpls-0.8.0.min.js deleted file mode 100644 index 8f4e630b9f..0000000000 --- a/app/assets/javascripts/shared/mm-foundation-tpls-0.8.0.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * angular-mm-foundation - * http://pineconellc.github.io/angular-foundation/ - - * Version: 0.8.0 - 2015-10-13 - * License: MIT - * (c) Pinecone, LLC - */ -angular.module("mm.foundation",["mm.foundation.tpls","mm.foundation.accordion","mm.foundation.alert","mm.foundation.bindHtml","mm.foundation.buttons","mm.foundation.position","mm.foundation.mediaQueries","mm.foundation.dropdownToggle","mm.foundation.interchange","mm.foundation.transition","mm.foundation.modal","mm.foundation.offcanvas","mm.foundation.pagination","mm.foundation.tooltip","mm.foundation.popover","mm.foundation.progressbar","mm.foundation.rating","mm.foundation.tabs","mm.foundation.topbar","mm.foundation.tour","mm.foundation.typeahead"]),angular.module("mm.foundation.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/topbar/has-dropdown.html","template/topbar/toggle-top-bar.html","template/topbar/top-bar-dropdown.html","template/topbar/top-bar-section.html","template/topbar/top-bar.html","template/tour/tour.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("mm.foundation.accordion",[]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(a){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(b,c,d,e){var f,g;e.addGroup(b),b.isOpen=!1,d.isOpen&&(f=a(d.isOpen),g=f.assign,b.$parent.$watch(f,function(a){b.isOpen=!!a})),b.$watch("isOpen",function(a){a&&e.closeOthers(b),g&&g(b.$parent,a)})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",compile:function(a,b,c){return function(a,b,d,e){e.setHeading(c(a,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("mm.foundation.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b&&"undefined"!=typeof b.close}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"=",close:"&"}}}),angular.module("mm.foundation.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("mm.foundation.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass,this.toggleEvent=a.toggleEvent}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){b.hasClass(e.activeClass)||a.$apply(function(){f.$setViewValue(a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("mm.foundation.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].body.scrollTop||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].body.scrollLeft||a[0].documentElement.scrollLeft)}}}}]),angular.module("mm.foundation.mediaQueries",[]).factory("matchMedia",["$document","$window",function(a,b){return b.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a[0])}]).factory("mediaQueries",["$document","matchMedia",function(a,b){var c=angular.element(a[0].querySelector("head"));c.append(''),c.append(''),c.append(''),c.append('');var d=/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g,e={topbar:getComputedStyle(c[0].querySelector("meta.foundation-mq-topbar")).fontFamily.replace(d,""),small:getComputedStyle(c[0].querySelector("meta.foundation-mq-small")).fontFamily.replace(d,""),medium:getComputedStyle(c[0].querySelector("meta.foundation-mq-medium")).fontFamily.replace(d,""),large:getComputedStyle(c[0].querySelector("meta.foundation-mq-large")).fontFamily.replace(d,"")};return{topbarBreakpoint:function(){return!b(e.topbar).matches},small:function(){return b(e.small).matches},medium:function(){return b(e.medium).matches},large:function(){return b(e.large).matches}}}]),angular.module("mm.foundation.dropdownToggle",["mm.foundation.position","mm.foundation.mediaQueries"]).controller("DropdownToggleController",["$scope","$attrs","mediaQueries",function(a,b,c){this.small=function(){return c.small()&&!c.medium()}}]).directive("dropdownToggle",["$document","$window","$location","$position",function(a,b,c,d){var e=null,f=angular.noop;return{restrict:"CA",controller:"DropdownToggleController",link:function(c,g,h,i){var j=g.parent(),k=angular.element(a[0].querySelector(h.dropdownToggle)),l=function(){return j.hasClass("has-dropdown")},m=function(c){k=angular.element(a[0].querySelector(h.dropdownToggle));var m=g===e;if(c.preventDefault(),c.stopPropagation(),e&&f(),!m&&!g.hasClass("disabled")&&!g.prop("disabled")){k.css("display","block"),k.addClass("f-open-dropdown");var n=d.offset(g),o=d.offset(angular.element(k[0].offsetParent)),p=k.prop("offsetWidth"),q={top:n.top-o.top+n.height+"px"};if(i.small())q.left=Math.max((o.width-p)/2,8)+"px",q.position="absolute",q.width="95%",q["max-width"]="none";else{var r=Math.round(n.left-o.left),s=b.innerWidth-p-8;r>s&&(r=s,k.removeClass("left").addClass("right")),q.left=r+"px",q.position=null,q["max-width"]=null}k.css(q),g.addClass("expanded"),l()&&j.addClass("hover"),e=g,f=function(){a.off("click",f),k.css("display","none"),k.removeClass("f-open-dropdown"),g.removeClass("expanded"),f=angular.noop,e=null,j.hasClass("hover")&&j.removeClass("hover")},a.on("click",f)}};k&&k.css("display","none"),c.$watch("$location.path",function(){f()}),g.on("click",m),g.on("$destroy",function(){g.off("click",m)})}}}]),angular.module("mm.foundation.interchange",["mm.foundation.mediaQueries"]).factory("interchangeQueries",["$document",function(a){for(var b,c,d={"default":"only screen",landscape:"only screen and (orientation: landscape)",portrait:"only screen and (orientation: portrait)",retina:"only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx)"},e="foundation-mq-",f=["small","medium","large","xlarge","xxlarge"],g=angular.element(a[0].querySelector("head")),h=0;h'),b=getComputedStyle(g[0].querySelector("meta."+e+f[h])),c=b.fontFamily.replace(/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g,""),d[f[h]]=c;return d}]).factory("interchangeQueriesManager",["interchangeQueries",function(a){return{add:function(b,c){return b&&c&&angular.isString(b)&&angular.isString(c)&&!a[b]?(a[b]=c,!0):!1}}}]).factory("interchangeTools",["$window","matchMedia","interchangeQueries",function(a,b,c){var d=function(a){for(var b,c=a.split(/\[(.*?)\]/),d=c.length,e=/^(.+)\,\ \((.+)\)$/,f={};d--;)c[d].replace(/[\W\d]+/,"").length>4&&(b=e.exec(c[d]),b&&3===b.length&&(f[b[2]]=b[1]));return f},e=function(a){var d,e,f;for(d in a)if(e=c[d]||d,f=b(e),f.matches)return a[d]};return{parseAttribute:d,findCurrentMediaFile:e}}]).directive("interchange",["$window","$rootScope","interchangeTools",function(a,b,c){var d=/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)\ *,/i;return{restrict:"A",scope:!0,priority:450,compile:function(e,f){return"DIV"!==e[0].nodeName||d.test(f.interchange)||e.html(''),{pre:function(){},post:function(d,e,f){var g,h;switch(h=e&&e[0]&&e[0].nodeName,d.fileMap=c.parseAttribute(f.interchange),h){case"DIV":g=c.findCurrentMediaFile(d.fileMap),d.type=/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)$/i.test(g)?"background":"include";break;case"IMG":d.type="image";break;default:return}var i=function(a){var f=c.findCurrentMediaFile(d.fileMap);if(!d.currentFile||d.currentFile!==f){switch(d.currentFile=f,d.type){case"image":e.attr("src",d.currentFile);break;case"background":e.css("background-image","url("+d.currentFile+")")}b.$emit("replace",e,d),a&&d.$apply()}};i(),a.addEventListener("resize",i),d.$on("$destroy",function(){a.removeEventListener("resize",i)})}}}}}]),angular.module("mm.foundation.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("mm.foundation.modal",["mm.foundation.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0?c[0].querySelectorAll("[autofocus]")[0].focus():c[0].focus()})}}}]).factory("$modalStack",["$window","$transition","$timeout","$document","$compile","$rootScope","$$stackedMap",function(a,b,c,d,e,f,g){function h(){for(var a=-1,b=q.keys(),c=0;c0),j()})}function j(){if(m&&-1==h()){var a=n;k(m,n,150,function(){a.$destroy(),a=null}),m=void 0,n=void 0}}function k(a,d,e,f){function g(){g.done||(g.done=!0,a.remove(),f&&f())}d.animate=!1;var h=b.transitionEndEventName;if(h){var i=c(g,e);a.bind(h,function(){c.cancel(i),g(),d.$apply()})}else c(g,0)}function l(b,c){angular.isUndefined(c)&&(c=0);var d=a.pageYOffset||0;return c+d}var m,n,o,p="modal-open",q=g.createNew(),r={};return f.$watch(h,function(a){n&&(n.index=a)}),d.bind("keydown",function(a){var b;27===a.which&&(b=q.top(),b&&b.value.keyboard&&f.$apply(function(){r.dismiss(b.key)}))}),r.open=function(b,c){b.options={deferred:c.deferred,modalScope:c.scope,backdrop:c.backdrop,keyboard:c.keyboard,parent:c.parent},q.add(b,b.options);var g=d.find(c.parent).eq(0),i=h();i>=0&&!m&&(n=f.$new(!0),n.index=i,m=e("
")(n),g.append(m));var j=angular.element('
');g.append(j[0]),o=parseInt(a.getComputedStyle(j[0]).top)||0;var k=l(j,o);j.remove();var r=angular.element('
').attr({"window-class":c.windowClass,index:q.length()-1,animate:"animate"});r.html(c.content);var s=e(r)(c.scope);q.top().value.modalDomEl=s,g.append(s),g.addClass(p)},r.reposition=function(a){var b=q.get(a).value;if(b){var c=b.modalDomEl,d=l(c,o);c.css("top",d+"px")}},r.close=function(a,b){var c=q.get(a);c&&(c.value.deferred.resolve(b),i(a))},r.dismiss=function(a,b){var c=q.get(a);c&&(c.value.deferred.reject(b),i(a))},r.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},r.getTop=function(){return q.top()},r}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)},reposition:function(){h.reposition(k)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i),b.controllerAs&&(d[b.controllerAs]=f)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass,parent:b.parent||"body"})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("mm.foundation.offcanvas",[]).directive("offCanvasWrap",["$window",function(a){return{scope:{},restrict:"C",link:function(b,c){var d=angular.element(a),e=b.sidebar=c;b.hide=function(){e.removeClass("move-left"),e.removeClass("move-right")},d.bind("resize.body",b.hide),b.$on("$destroy",function(){d.unbind("resize.body",b.hide)})},controller:["$scope",function(a){this.leftToggle=function(){a.sidebar.toggleClass("move-right")},this.rightToggle=function(){a.sidebar.toggleClass("move-left")},this.hide=function(){a.hide()}}]}}]).directive("leftOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.leftToggle()})}}}]).directive("rightOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.rightToggle()})}}}]).directive("exitOffCanvas",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]).directive("offCanvasList",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]),angular.module("mm.foundation.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(a,b,c,d){var e=this,f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d){b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){e.itemsPerPage=parseInt(b,10),a.totalPages=e.calculateTotalPages()}):this.itemsPerPage=d},this.noPrevious=function(){return 1===this.page},this.noNext=function(){return this.page===a.totalPages},this.isActive=function(a){return this.page===a},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.getAttributeValue=function(b,c,e){return angular.isDefined(b)?e?d(b)(a.$parent):a.$parent.$eval(b):c},this.render=function(){this.page=parseInt(a.page,10)||1,this.page>0&&this.page<=a.totalPages&&(a.pages=this.getPages(this.page,a.totalPages))},a.selectPage=function(b){!e.isActive(b)&&b>0&&b<=a.totalPages&&(a.page=b,a.onSelectPage({page:b}))},a.$watch("page",function(){e.render()}),a.$watch("totalItems",function(){a.totalPages=e.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),e.page>b?a.selectPage(b):e.render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c,d){return{number:a,text:b,active:c,disabled:d}}var h,i=f.getAttributeValue(e.boundaryLinks,b.boundaryLinks),j=f.getAttributeValue(e.directionLinks,b.directionLinks),k=f.getAttributeValue(e.firstText,b.firstText,!0),l=f.getAttributeValue(e.previousText,b.previousText,!0),m=f.getAttributeValue(e.nextText,b.nextText,!0),n=f.getAttributeValue(e.lastText,b.lastText,!0),o=f.getAttributeValue(e.rotate,b.rotate);f.init(b.itemsPerPage),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){h=parseInt(a,10),f.render()}),f.getPages=function(a,b){var c=[],d=1,e=b,p=angular.isDefined(h)&&b>h;p&&(o?(d=Math.max(a-Math.floor(h/2),1),e=d+h-1,e>b&&(e=b,d=e-h+1)):(d=(Math.ceil(a/h)-1)*h+1,e=Math.min(d+h-1,b)));for(var q=d;e>=q;q++){var r=g(q,q,f.isActive(q),!1);c.push(r)}if(p&&!o){if(d>1){var s=g(d-1,"...",!1,!1);c.unshift(s)}if(b>e){var t=g(e+1,"...",!1,!1);c.push(t)}}if(j){var u=g(a-1,l,!1,f.noPrevious());c.unshift(u);var v=g(a+1,m,!1,f.noNext());c.push(v)}if(i){var w=g(1,k,!1,f.noPrevious());c.unshift(w);var x=g(b,n,!1,f.noNext());c.push(x)}return c}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){function f(a,b,c,d,e){return{number:a,text:b,disabled:c,previous:i&&d,next:i&&e}}var g=e.getAttributeValue(d.previousText,a.previousText,!0),h=e.getAttributeValue(d.nextText,a.nextText,!0),i=e.getAttributeValue(d.align,a.align);e.init(a.itemsPerPage),e.getPages=function(a){return[f(a-1,g,e.noPrevious(),!0,!1),f(a+1,h,e.noNext(),!1,!0)]}}}}]),angular.module("mm.foundation.tooltip",["mm.foundation.position","mm.foundation.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!z||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return b.tt_content?(r(),u&&g.cancel(u),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),A(),b.tt_isOpen=!0,b.$digest(),A):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),b.tt_animation?u=g(s,500):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=!1,z=angular.isDefined(d[l+"Enable"]),A=function(){var a,d,e,f;switch(a=w?j.offset(c):j.position(c),d=t.prop("offsetWidth"),e=t.prop("offsetHeight"),b.tt_placement){case"right":f={top:a.top+a.height/2-e/2,left:a.left+a.width+10};break;case"bottom":f={top:a.top+a.height+10,left:a.left};break;case"left":f={top:a.top+a.height/2-e/2,left:a.left-d-10};break;default:f={top:a.top-e-10,left:a.left}}f.top+="px",f.left+="px",t.css(f)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d[l+"Placement"]=d[l+"Placement"]||null,d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)&&a?a:o.placement}),d[l+"PopupDelay"]=d[l+"PopupDelay"]||null,d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var B=function(){y&&(angular.isFunction(x.show)?C():(c.unbind(x.show,k),c.unbind(x.hide,m)))},C=function(){};d[l+"Trigger"]=d[l+"Trigger"]||null,d.$observe(l+"Trigger",function(a){B(),C(),x=n(a),angular.isFunction(x.show)?C=b.$watch(function(){return x.show(b,c,d)},function(a){return g(a?p:q)}):x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m)),y=!0});var D=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(D)?!!D:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),B(),C(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("mm.foundation.popover",["mm.foundation.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("mm.foundation.progressbar",["mm.foundation.transition"]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(a,b,c,d){var e=this,f=[],g=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,h=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.addBar=function(a,b){var c=0,d=a.$parent.$index;angular.isDefined(d)&&f[d]&&(c=f[d].value),f.push(a),this.update(b,a.value,c),a.$watch("value",function(a,c){a!==c&&e.update(b,a,c)}),a.$on("$destroy",function(){e.removeBar(a)})},this.update=function(a,b,c){var e=this.getPercentage(b);h?(a.css("width",this.getPercentage(c)+"%"),d(a,{width:e+"%"})):a.css({transition:"none",width:e+"%"})},this.removeBar=function(a){f.splice(f.indexOf(a),1)},this.getPercentage=function(a){return Math.round(100*a/g)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},template:'
'}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("mm.foundation.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(a,b,c,d){this.maxRange=angular.isDefined(b.max)?a.$parent.$eval(b.max):d.max,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):d.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):d.stateOff,this.createRateObjects=function(a){for(var b={stateOn:this.stateOn,stateOff:this.stateOff},c=0,d=a.length;d>c;c++)a[c]=angular.extend({index:c},b,a[c]);return a},a.range=this.createRateObjects(angular.isDefined(b.ratingStates)?angular.copy(a.$parent.$eval(b.ratingStates)):new Array(this.maxRange)),a.rate=function(b){a.value===b||a.readonly||(a.value=b)},a.enter=function(b){a.readonly||(a.val=b),a.onHover({value:b})},a.reset=function(){a.val=angular.copy(a.value),a.onLeave()},a.$watch("value",function(b){a.val=b}),a.readonly=!1,b.readonly&&a.$parent.$watch(c(b.readonly),function(b){a.readonly=!!b})}]).directive("rating",function(){return{restrict:"EA",scope:{value:"=",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0}}),angular.module("mm.foundation.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];angular.isUndefined(a.openOnLoad)&&(a.openOnLoad=!0),b.select=function(a){angular.forEach(c,function(a){a.active=!1}),a.active=!0},b.addTab=function(d){c.push(d),a.openOnLoad&&(1===c.length||d.active)&&b.select(d)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{openOnLoad:"=?"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1,a.type=angular.isDefined(c.type)?a.$parent.$eval(c.type):"tabs"}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){var g,h;e.active?(g=a(e.active),h=g.assign,b.$parent.$watch(g,function(a,c){a!==c&&(b.active=!!a)}),b.active=g(b.$parent)):h=g=angular.noop,b.$watch("active",function(a){angular.isFunction(h)&&(h(b.$parent,a),a?(f.select(b),b.onSelect()):b.onDeselect())}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("mm.foundation.topbar",["mm.foundation.mediaQueries"]).factory("closest",[function(){return function(a,b){for(var c=function(a,b){for(var c=(a.parentNode||a.document).querySelectorAll(b),d=-1;c[++d]&&c[d]!=a;);return!!c[d]},d=a[0];d;){if(c(d,b))return angular.element(d);d=d.parentElement}return!1}}]).directive("topBar",["$timeout","$compile","$window","$document","mediaQueries",function(a,b,c,d,e){return{scope:{stickyClass:"@",backText:"@",stickyOn:"=",customBackText:"=",isHover:"=",mobileShowParentLink:"=",scrolltop:"="},restrict:"EA",replace:!0,templateUrl:"template/topbar/top-bar.html",transclude:!0,controller:["$window","$scope","closest",function(b,c,f){c.settings={},c.settings.stickyClass=c.stickyClass||"sticky",c.settings.backText=c.backText||"Back",c.settings.stickyOn=c.stickyOn||"all",c.settings.customBackText=void 0===c.customBackText?!0:c.customBackText,c.settings.isHover=void 0===c.isHover?!0:c.isHover,c.settings.mobileShowParentLink=void 0===c.mobileShowParentLink?!0:c.mobileShowParentLink,c.settings.scrolltop=void 0===c.scrolltop?!0:c.scrolltop,this.settings=c.settings,c.index=0;var g=function(a){var b=a.offsetHeight,c=a.currentStyle||getComputedStyle(a);return b+=parseInt(c.marginTop,10)+parseInt(c.marginBottom,10)},h=[];this.addSection=function(a){h.push(a)},this.removeSection=function(a){var b=h.indexOf(a);b>-1&&h.splice(b,1)};var i=/rtl/i.test(d.find("html").attr("dir"))?"right":"left";c.$watch("index",function(a){for(var b=0;bb&&!g.hasClass("fixed")?(g.addClass("fixed"),h.css("padding-top",a.originalHeight+"px")):c.pageYOffset<=b&&g.hasClass("fixed")&&(g.removeClass("fixed"),h.css("padding-top",""))}},l=function(){var b=e.topbarBreakpoint();if(i!==b){i=e.topbarBreakpoint(),f.removeClass("expanded"),f.parent().removeClass("expanded"),a.height="";var c=angular.element(f[0].querySelectorAll("section"));angular.forEach(c,function(a){angular.element(a.querySelectorAll("li.moved")).removeClass("moved")}),a.$apply()}},m=function(){k(),a.$apply()};if(a.toggle=function(b){if(!e.topbarBreakpoint())return!1;var d=void 0===b?!f.hasClass("expanded"):b;d?f.addClass("expanded"):f.removeClass("expanded"),a.settings.scrolltop?!d&&f.hasClass("fixed")?(f.parent().addClass("fixed"),f.removeClass("fixed"),h.css("padding-top",a.originalHeight+"px")):d&&f.parent().hasClass("fixed")&&(f.parent().removeClass("fixed"),f.addClass("fixed"),h.css("padding-top",""),c.scrollTo(0,0)):(j()&&f.parent().addClass("fixed"),f.parent().hasClass("fixed")&&(d?(f.addClass("fixed"),f.parent().addClass("expanded"),h.css("padding-top",a.originalHeight+"px")):(f.removeClass("fixed"),f.parent().removeClass("expanded"),k())))},g.hasClass("fixed")||j()){a.stickyTopbar=!0,a.height=g[0].offsetHeight;var n=g[0].getBoundingClientRect().top+c.pageYOffset}else a.height=f[0].offsetHeight;a.originalHeight=a.height,a.$watch("height",function(a){a?f.css("height",a+"px"):f.css("height","")}),angular.element(c).bind("resize",l),angular.element(c).bind("scroll",m),a.$on("$destroy",function(){angular.element(c).unbind("scroll",l),angular.element(c).unbind("resize",m)}),g.hasClass("fixed")&&h.css("padding-top",a.originalHeight+"px")}}}]).directive("toggleTopBar",["closest",function(a){return{scope:{},require:"^topBar",restrict:"A",replace:!0,templateUrl:"template/topbar/toggle-top-bar.html",transclude:!0,link:function(b,c,d,e){c.bind("click",function(b){var c=a(angular.element(b.currentTarget),"li");c.hasClass("back")||c.hasClass("has-dropdown")||e.toggle()}),b.$on("$destroy",function(){c.unbind("click")})}}}]).directive("topBarSection",["$compile","closest",function(a,b){return{scope:{},require:"^topBar",restrict:"EA",replace:!0,templateUrl:"template/topbar/top-bar-section.html",transclude:!0,link:function(a,c,d,e){var f=c;a.reset=function(){angular.element(f[0].querySelectorAll("li.moved")).removeClass("moved")},a.move=function(a,b){f.css("left"===a?{left:-100*b+"%"}:{right:-100*b+"%"})},e.addSection(a),a.$on("$destroy",function(){e.removeSection(a)});var g=f[0].querySelectorAll("li>a");angular.forEach(g,function(c){var d=angular.element(c),f=b(d,"li");f.hasClass("has-dropdown")||f.hasClass("back")||f.hasClass("title")||(d.bind("click",function(){e.toggle(!1)}),a.$on("$destroy",function(){d.bind("click")}))})}}}]).directive("hasDropdown",["mediaQueries",function(a){return{scope:{},require:"^topBar",restrict:"A",templateUrl:"template/topbar/has-dropdown.html",replace:!0,transclude:!0,link:function(b,c,d,e){b.triggerLink=c.children("a")[0];var f=angular.element(b.triggerLink);f.bind("click",function(a){e.forward(a)}),b.$on("$destroy",function(){f.unbind("click")}),c.bind("mouseenter",function(){e.settings.isHover&&!a.topbarBreakpoint()&&c.addClass("not-click")}),c.bind("click",function(){e.settings.isHover||a.topbarBreakpoint()||c.toggleClass("not-click")}),c.bind("mouseleave",function(){c.removeClass("not-click")}),b.$on("$destroy",function(){c.unbind("click"),c.unbind("mouseenter"),c.unbind("mouseleave")})},controller:["$window","$scope",function(a,b){this.triggerLink=b.triggerLink}]}}]).directive("topBarDropdown",["$compile",function(a){return{scope:{},require:["^topBar","^hasDropdown"],restrict:"A",replace:!0,templateUrl:"template/topbar/top-bar-dropdown.html",transclude:!0,link:function(b,c,d,e){var f,g=e[0],h=e[1],i=angular.element(h.triggerLink),j=i.attr("href");b.linkText=i.text(),b.back=function(a){g.back(a)},b.backText=g.settings.customBackText?g.settings.backText:"« "+i.html(),f=angular.element(g.settings.mobileShowParentLink&&j&&j.length>1?'
  • {{backText}}
  • {{linkText}}
  • ':'
  • {{backText}}
  • '),a(f)(b),c.prepend(f)}}}]),angular.module("mm.foundation.tour",["mm.foundation.position","mm.foundation.tooltip"]).service("$tour",["$window",function(a){function b(){try{return parseInt(a.localStorage.getItem("mm.tour.step"),10)}catch(b){if("SecurityError"!==b.name)throw b}}function c(){try{a.localStorage.setItem("mm.tour.step",e)}catch(b){if("SecurityError"!==b.name)throw b}}function d(a){e=a,c()}var e=b(),f={};this.add=function(a,b){f[a]=b},this.has=function(a){return!!f[a]},this.isActive=function(){return e>0},this.current=function(a){return a?void d(e):e},this.start=function(){d(1)},this.next=function(){d(e+1)},this.end=function(){d(0)}}]).directive("stepTextPopup",["$tour",function(a){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tour/tour.html",link:function(b,c){b.isLastStep=function(){return!a.has(a.current()+1)},b.endTour=function(){c.remove(),a.end()},b.nextStep=function(){c.remove(),a.next()},b.$on("$locationChangeSuccess",b.endTour)}}}]).directive("stepText",["$position","$tooltip","$tour","$window",function(a,b,c,d){function e(a){var b=a[0].getBoundingClientRect();return b.top>=0&&b.left>=0&&b.bottom<=d.innerHeight-80&&b.right<=d.innerWidth}function f(b,f,g){var h=parseInt(g.stepIndex,10);if(c.isActive()&&h&&(c.add(h,g),h===c.current())){if(!e(f)){var i=a.offset(f);d.scrollTo(0,i.top-d.innerHeight/2)}return!0}return!1}return b("stepText","step",f)}]),angular.module("mm.foundation.typeahead",["mm.foundation.position","mm.foundation.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+c+"'.");return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?b(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=angular.element("
    ");w.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&w.attr("template-url",k.typeaheadTemplateUrl);var x=i.$new();i.$on("$destroy",function(){x.$destroy()});var y=function(){x.matches=[],x.activeIdx=-1},z=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){if(a===l.$viewValue&&m)if(c.length>0){x.activeIdx=0,x.matches.length=0;for(var d=0;d=n?o>0?(A&&d.cancel(A),A=d(function(){z(a)},o)):z(a):(q(i,!1),y()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),x.select=function(a){var b,c,d={};d[v.itemName]=c=x.matches[a].model,b=v.modelMapper(i,d),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,d)}),y(),j[0].focus()},j.bind("keydown",function(a){0!==x.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(x.activeIdx=(x.activeIdx+1)%x.matches.length,x.$digest()):38===a.which?(x.activeIdx=(x.activeIdx?x.activeIdx:x.matches.length)-1,x.$digest()):13===a.which||9===a.which?x.$apply(function(){x.select(x.activeIdx)}):27===a.which&&(a.stopPropagation(),y(),x.$digest()))}),j.bind("blur",function(){m=!1}),j.bind("focus",function(){m=!0});var B=function(a){j[0]!==a.target&&(y(),x.$digest())};e.bind("click",B),i.$on("$destroy",function(){e.unbind("click",B)});var C=a(w)(x);t?e.find("body").append(C):j.after(C)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?b.replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
    \n {{heading}}\n
    \n
    \n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
    \n')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html","
    \n \n ×\n
    \n")}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'
    \n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'
    \n
    \n
    \n')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'\n')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'\n')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'\n \n \n\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'\n \n \n\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
    \n \n
    \n

    \n

    \n
    \n
    \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'\n')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
    \n')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
    \n \n
    \n')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n\n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
    \n {{heading}}\n
    \n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/topbar/has-dropdown.html",[]).run(["$templateCache",function(a){a.put("template/topbar/has-dropdown.html",'
  • ')}]),angular.module("template/topbar/toggle-top-bar.html",[]).run(["$templateCache",function(a){a.put("template/topbar/toggle-top-bar.html",'')}]),angular.module("template/topbar/top-bar-dropdown.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar-dropdown.html",'')}]),angular.module("template/topbar/top-bar-section.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar-section.html",'
    ')}]),angular.module("template/topbar/top-bar.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar.html",'')}]),angular.module("template/tour/tour.html",[]).run(["$templateCache",function(a){a.put("template/tour/tour.html",'
    \n \n
    \n

    \n

    \n Next\n End\n ×\n
    \n
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html","
      \n"+'
    • \n
      \n
    • \n
    \n')}]); diff --git a/app/assets/javascripts/shared/mm-foundation-tpls-0.9.0-20180826174721.min.js b/app/assets/javascripts/shared/mm-foundation-tpls-0.9.0-20180826174721.min.js new file mode 100644 index 0000000000..1df49b5e6e --- /dev/null +++ b/app/assets/javascripts/shared/mm-foundation-tpls-0.9.0-20180826174721.min.js @@ -0,0 +1,10 @@ +/* + * angular-mm-foundation + * http://pineconellc.github.io/angular-foundation/ + + * Version: 0.9.0-20180826174721 - 2018-08-27 + * License: MIT + * (c) Pinecone, LLC + */ +angular.module("mm.foundation",["mm.foundation.tpls","mm.foundation.accordion","mm.foundation.alert","mm.foundation.bindHtml","mm.foundation.buttons","mm.foundation.position","mm.foundation.mediaQueries","mm.foundation.dropdownToggle","mm.foundation.interchange","mm.foundation.transition","mm.foundation.modal","mm.foundation.offcanvas","mm.foundation.pagination","mm.foundation.tooltip","mm.foundation.popover","mm.foundation.progressbar","mm.foundation.rating","mm.foundation.tabs","mm.foundation.topbar","mm.foundation.tour","mm.foundation.typeahead"]),angular.module("mm.foundation.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/topbar/has-dropdown.html","template/topbar/toggle-top-bar.html","template/topbar/top-bar-dropdown.html","template/topbar/top-bar-section.html","template/topbar/top-bar.html","template/tour/tour.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("mm.foundation.accordion",[]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(c){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(a){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(b,c,d,e){var f,g;e.addGroup(b),b.isOpen=!1,d.isOpen&&(f=a(d.isOpen),g=f.assign,b.$parent.$watch(f,function(a){b.isOpen=!!a})),b.$watch("isOpen",function(a){a&&e.closeOthers(b),g&&g(b.$parent,a)})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",compile:function(a,b,c){return function(a,b,d,e){e.setHeading(c(a,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("mm.foundation.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b&&"undefined"!=typeof b.close}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"=",close:"&"}}}),angular.module("mm.foundation.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("mm.foundation.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass,this.toggleEvent=a.toggleEvent}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){b.hasClass(e.activeClass)||a.$apply(function(){f.$setViewValue(a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("mm.foundation.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].body.scrollTop||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].body.scrollLeft||a[0].documentElement.scrollLeft)}}}}]),angular.module("mm.foundation.mediaQueries",[]).factory("matchMedia",["$document","$window",function(a,b){return b.matchMedia||function(a,b){var c,d=a.documentElement,e=d.firstElementChild||d.firstChild,f=a.createElement("body"),g=a.createElement("div");return g.id="mq-test-1",g.style.cssText="position:absolute;top:-100em",f.style.background="none",f.appendChild(g),function(a){return g.innerHTML='­',d.insertBefore(f,e),c=42===g.offsetWidth,d.removeChild(f),{matches:c,media:a}}}(a[0])}]).factory("mediaQueries",["$document","matchMedia",function(a,b){var c=angular.element(a[0].querySelector("head"));c.append(''),c.append(''),c.append(''),c.append('');var d=/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g,e={topbar:getComputedStyle(c[0].querySelector("meta.foundation-mq-topbar")).fontFamily.replace(d,""),small:getComputedStyle(c[0].querySelector("meta.foundation-mq-small")).fontFamily.replace(d,""),medium:getComputedStyle(c[0].querySelector("meta.foundation-mq-medium")).fontFamily.replace(d,""),large:getComputedStyle(c[0].querySelector("meta.foundation-mq-large")).fontFamily.replace(d,"")};return{topbarBreakpoint:function(){return!b(e.topbar).matches},small:function(){return b(e.small).matches},medium:function(){return b(e.medium).matches},large:function(){return b(e.large).matches}}}]),angular.module("mm.foundation.dropdownToggle",["mm.foundation.position","mm.foundation.mediaQueries"]).controller("DropdownToggleController",["$scope","$attrs","mediaQueries",function(a,b,c){this.small=function(){return c.small()&&!c.medium()}}]).directive("dropdownToggle",["$document","$window","$location","$position",function(a,b,c,d){var e=null,f=angular.noop;return{restrict:"CA",controller:"DropdownToggleController",link:function(c,g,h,i){var j=g.parent(),k=angular.element(a[0].querySelector(h.dropdownToggle)),l=function(){return j.hasClass("has-dropdown")},m=function(c){k=angular.element(a[0].querySelector(h.dropdownToggle));var m=g===e;if(c.preventDefault(),c.stopPropagation(),e&&f(),!m&&!g.hasClass("disabled")&&!g.prop("disabled")){k.css("display","block"),k.addClass("f-open-dropdown");var n=d.offset(g),o=d.offset(angular.element(k[0].offsetParent)),p=k.prop("offsetWidth"),q={top:n.top-o.top+n.height+"px"};if(i.small())q.left=Math.max((o.width-p)/2,8)+"px",q.position="absolute",q.width="95%",q["max-width"]="none";else{var r=Math.round(n.left-o.left),s=b.innerWidth-p-8;r>s&&(r=s,k.removeClass("left").addClass("right")),q.left=r+"px",q.position=null,q["max-width"]=null}k.css(q),g.addClass("expanded"),l()&&j.addClass("hover"),e=g,f=function(b){a.off("click",f),k.css("display","none"),k.removeClass("f-open-dropdown"),g.removeClass("expanded"),f=angular.noop,e=null,j.hasClass("hover")&&j.removeClass("hover")},a.on("click",f)}};k&&k.css("display","none"),c.$watch("$location.path",function(){f()}),g.on("click",m),g.on("$destroy",function(){g.off("click",m)})}}}]),angular.module("mm.foundation.interchange",["mm.foundation.mediaQueries"]).factory("interchangeQueries",["$document",function(a){for(var b,c,d={"default":"only screen",landscape:"only screen and (orientation: landscape)",portrait:"only screen and (orientation: portrait)",retina:"only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx)"},e="foundation-mq-",f=["small","medium","large","xlarge","xxlarge"],g=angular.element(a[0].querySelector("head")),h=0;h'),b=getComputedStyle(g[0].querySelector("meta."+e+f[h])),c=b.fontFamily.replace(/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g,""),d[f[h]]=c;return d}]).factory("interchangeQueriesManager",["interchangeQueries",function(a){return{add:function(b,c){return b&&c&&angular.isString(b)&&angular.isString(c)&&!a[b]?(a[b]=c,!0):!1}}}]).factory("interchangeTools",["$window","matchMedia","interchangeQueries",function(a,b,c){var d=function(a){for(var b,c=a.split(/\[(.*?)\]/),d=c.length,e=/^(.+)\,\ \((.+)\)$/,f={};d--;)c[d].replace(/[\W\d]+/,"").length>4&&(b=e.exec(c[d]),b&&3===b.length&&(f[b[2]]=b[1]));return f},e=function(a){var d,e,f;for(d in a)if(e=c[d]||d,f=b(e),f.matches)return a[d]};return{parseAttribute:d,findCurrentMediaFile:e}}]).directive("interchange",["$window","$rootScope","interchangeTools",function(a,b,c){var d=/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)\ *,/i;return{restrict:"A",scope:!0,priority:450,compile:function(e,f){return"DIV"!==e[0].nodeName||d.test(f.interchange)||e.html(''),{pre:function(a,b,c){},post:function(d,e,f){var g,h;switch(h=e&&e[0]&&e[0].nodeName,d.fileMap=c.parseAttribute(f.interchange),h){case"DIV":g=c.findCurrentMediaFile(d.fileMap),/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)$/i.test(g)?d.type="background":d.type="include";break;case"IMG":d.type="image";break;default:return}var i=function(a){var f=c.findCurrentMediaFile(d.fileMap);if(!d.currentFile||d.currentFile!==f){switch(d.currentFile=f,d.type){case"image":e.attr("src",d.currentFile);break;case"background":e.css("background-image","url("+d.currentFile+")")}b.$emit("replace",e,d),a&&d.$apply()}};i(),a.addEventListener("resize",i),d.$on("$destroy",function(){a.removeEventListener("resize",i)})}}}}}]),angular.module("mm.foundation.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(a){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("mm.foundation.modal",["mm.foundation.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0?c[0].querySelectorAll("[autofocus]")[0].focus():c[0].focus()})}}}]).factory("$modalStack",["$window","$transition","$timeout","$document","$compile","$rootScope","$$stackedMap",function(a,b,c,d,e,f,g){function h(){for(var a=-1,b=q.keys(),c=0;c0),j()})}function j(){if(m&&-1==h()){var a=n;k(m,n,150,function(){a.$destroy(),a=null}),m=void 0,n=void 0}}function k(a,d,e,f){function g(){g.done||(g.done=!0,a.remove(),f&&f())}d.animate=!1;var h=b.transitionEndEventName;if(h){var i=c(g,e);a.bind(h,function(){c.cancel(i),g(),d.$apply()})}else c(g,0)}function l(b,c){angular.isUndefined(c)&&(c=0);var d=a.pageYOffset||0;return c+d}var m,n,o,p="modal-open",q=g.createNew(),r={};return f.$watch(h,function(a){n&&(n.index=a)}),d.bind("keydown",function(a){var b;27===a.which&&(b=q.top(),b&&b.value.keyboard&&f.$apply(function(){r.dismiss(b.key)}))}),r.open=function(b,c){b.options={deferred:c.deferred,modalScope:c.scope,backdrop:c.backdrop,keyboard:c.keyboard,parent:c.parent},q.add(b,b.options);var g=d.find(c.parent).eq(0),i=h();i>=0&&!m&&(n=f.$new(!0),n.index=i,m=e("
    ")(n),g.append(m));var j=angular.element('
    ');g.append(j[0]),o=parseInt(a.getComputedStyle(j[0]).top)||0;var k=l(j,o);j.remove();var r=angular.element('
    ').attr({"window-class":c.windowClass,index:q.length()-1,animate:"animate"});r.html(c.content);var s=e(r)(c.scope);q.top().value.modalDomEl=s,g.append(s),g.addClass(p)},r.reposition=function(a){var b=q.get(a).value;if(b){var c=b.modalDomEl,d=l(c,o);c.css("top",d+"px")}},r.close=function(a,b){var c=q.get(a);c&&(c.value.deferred.resolve(b),i(a))},r.dismiss=function(a,b){var c=q.get(a);c&&(c.value.deferred.reject(b),i(a))},r.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},r.getTop=function(){return q.top()},r}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a,e){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)},reposition:function(){h.reposition(k)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i),b.controllerAs&&(d[b.controllerAs]=f)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass,parent:b.parent||"body"})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("mm.foundation.offcanvas",[]).directive("offCanvasWrap",["$window",function(a){return{scope:{},restrict:"C",link:function(b,c,d){var e=angular.element(a),f=b.sidebar=c;b.hide=function(){f.removeClass("move-left"),f.removeClass("move-right")},e.bind("resize.body",b.hide),b.$on("$destroy",function(){e.unbind("resize.body",b.hide)})},controller:["$scope",function(a){this.leftToggle=function(){a.sidebar.toggleClass("move-right")},this.rightToggle=function(){a.sidebar.toggleClass("move-left")},this.hide=function(){a.hide()}}]}}]).directive("leftOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.leftToggle()})}}}]).directive("rightOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.rightToggle()})}}}]).directive("exitOffCanvas",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]).directive("offCanvasList",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]),angular.module("mm.foundation.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(a,b,c,d){var e=this,f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d){b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){e.itemsPerPage=parseInt(b,10),a.totalPages=e.calculateTotalPages()}):this.itemsPerPage=d},this.noPrevious=function(){return 1===this.page},this.noNext=function(){return this.page===a.totalPages},this.isActive=function(a){return this.page===a},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.getAttributeValue=function(b,c,e){return angular.isDefined(b)?e?d(b)(a.$parent):a.$parent.$eval(b):c},this.render=function(){this.page=parseInt(a.page,10)||1,this.page>0&&this.page<=a.totalPages&&(a.pages=this.getPages(this.page,a.totalPages))},a.selectPage=function(b){!e.isActive(b)&&b>0&&b<=a.totalPages&&(a.page=b,a.onSelectPage({page:b}))},a.$watch("page",function(){e.render()}),a.$watch("totalItems",function(){a.totalPages=e.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),e.page>b?a.selectPage(b):e.render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c,d){return{number:a,text:b,active:c,disabled:d}}var h,i=f.getAttributeValue(e.boundaryLinks,b.boundaryLinks),j=f.getAttributeValue(e.directionLinks,b.directionLinks),k=f.getAttributeValue(e.firstText,b.firstText,!0),l=f.getAttributeValue(e.previousText,b.previousText,!0),m=f.getAttributeValue(e.nextText,b.nextText,!0),n=f.getAttributeValue(e.lastText,b.lastText,!0),o=f.getAttributeValue(e.rotate,b.rotate);f.init(b.itemsPerPage),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){h=parseInt(a,10),f.render()}),f.getPages=function(a,b){var c=[],d=1,e=b,p=angular.isDefined(h)&&b>h;p&&(o?(d=Math.max(a-Math.floor(h/2),1),e=d+h-1,e>b&&(e=b,d=e-h+1)):(d=(Math.ceil(a/h)-1)*h+1,e=Math.min(d+h-1,b)));for(var q=d;e>=q;q++){var r=g(q,q,f.isActive(q),!1);c.push(r)}if(p&&!o){if(d>1){var s=g(d-1,"...",!1,!1);c.unshift(s)}if(b>e){var t=g(e+1,"...",!1,!1);c.push(t)}}if(j){var u=g(a-1,l,!1,f.noPrevious());c.unshift(u);var v=g(a+1,m,!1,f.noNext());c.push(v)}if(i){var w=g(1,k,!1,f.noPrevious());c.unshift(w);var x=g(b,n,!1,f.noNext());c.push(x)}return c}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){function f(a,b,c,d,e){return{number:a,text:b,disabled:c,previous:i&&d,next:i&&e}}var g=e.getAttributeValue(d.previousText,a.previousText,!0),h=e.getAttributeValue(d.nextText,a.nextText,!0),i=e.getAttributeValue(d.align,a.align);e.init(a.itemsPerPage),e.getPages=function(a){return[f(a-1,g,e.noPrevious(),!0,!1),f(a+1,h,e.noNext(),!1,!0)]}}}}]),angular.module("mm.foundation.tooltip",["mm.foundation.position","mm.foundation.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
    ';return{restrict:"EA",scope:!0,compile:function(a,b){var c=f(s);return function(a,b,d){function f(){a.tt_isOpen?m():k()}function k(){(!z||a.$eval(d[l+"Enable"]))&&(a.tt_popupDelay?(v=g(p,a.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){a.$apply(function(){q()})}function p(){return a.tt_content?(r(),u&&g.cancel(u),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):b.after(t),A(),a.tt_isOpen=!0,a.$digest(),A):angular.noop}function q(){a.tt_isOpen=!1,g.cancel(v),a.tt_animation?u=g(s,500):s()}function r(){t&&s(),t=c(a,function(){}),a.$digest()}function s(){t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=!1,z=angular.isDefined(d[l+"Enable"]),A=function(){var c,d,e,f;switch(c=w?j.offset(b):j.position(b),d=t.prop("offsetWidth"),e=t.prop("offsetHeight"),a.tt_placement){case"right":f={top:c.top+c.height/2-e/2,left:c.left+c.width+10};break;case"bottom":f={top:c.top+c.height+10,left:c.left};break;case"left":f={top:c.top+c.height/2-e/2,left:c.left-d-10};break;default:f={top:c.top-e-10,left:c.left}}f.top+="px",f.left+="px",t.css(f)};a.tt_isOpen=!1,d.$observe(e,function(b){a.tt_content=b,!b&&a.tt_isOpen&&q()}),d.$observe(l+"Title",function(b){a.tt_title=b}),d[l+"Placement"]=d[l+"Placement"]||null,d.$observe(l+"Placement",function(b){a.tt_placement=angular.isDefined(b)&&b?b:o.placement}),d[l+"PopupDelay"]=d[l+"PopupDelay"]||null,d.$observe(l+"PopupDelay",function(b){var c=parseInt(b,10);a.tt_popupDelay=isNaN(c)?o.popupDelay:c});var B=function(){y&&(angular.isFunction(x.show)?C():(b.unbind(x.show,k),b.unbind(x.hide,m)))},C=function(){};d[l+"Trigger"]=d[l+"Trigger"]||null,d.$observe(l+"Trigger",function(c){B(),C(),x=n(c),angular.isFunction(x.show)?C=a.$watch(function(){return x.show(a,b,d)},function(a){return g(a?p:q)}):x.show===x.hide?b.bind(x.show,f):(b.bind(x.show,k),b.bind(x.hide,m)),y=!0});var D=a.$eval(d[l+"Animation"]);a.tt_animation=angular.isDefined(D)?!!D:o.animation,d.$observe(l+"AppendToBody",function(b){w=angular.isDefined(b)?h(b)(a):w}),w&&a.$on("$locationChangeSuccess",function(){a.tt_isOpen&&q()}),a.$on("$destroy",function(){g.cancel(u),g.cancel(v),B(),C(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("mm.foundation.popover",["mm.foundation.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("mm.foundation.progressbar",["mm.foundation.transition"]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(a,b,c,d){var e=this,f=[],g=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,h=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.addBar=function(a,b){var c=0,d=a.$parent.$index;angular.isDefined(d)&&f[d]&&(c=f[d].value),f.push(a),this.update(b,a.value,c),a.$watch("value",function(a,c){a!==c&&e.update(b,a,c)}),a.$on("$destroy",function(){e.removeBar(a)})},this.update=function(a,b,c){var e=this.getPercentage(b);h?(a.css("width",this.getPercentage(c)+"%"),d(a,{width:e+"%"})):a.css({transition:"none",width:e+"%"})},this.removeBar=function(a){f.splice(f.indexOf(a),1)},this.getPercentage=function(a){return Math.round(100*a/g)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},template:'
    '}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("mm.foundation.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(a,b,c,d){this.maxRange=angular.isDefined(b.max)?a.$parent.$eval(b.max):d.max,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):d.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):d.stateOff,this.createRateObjects=function(a){for(var b={stateOn:this.stateOn,stateOff:this.stateOff},c=0,d=a.length;d>c;c++)a[c]=angular.extend({index:c},b,a[c]);return a},a.range=angular.isDefined(b.ratingStates)?this.createRateObjects(angular.copy(a.$parent.$eval(b.ratingStates))):this.createRateObjects(new Array(this.maxRange)),a.rate=function(b){a.value===b||a.readonly||(a.value=b)},a.enter=function(b){a.readonly||(a.val=b),a.onHover({value:b})},a.reset=function(){a.val=angular.copy(a.value),a.onLeave()},a.$watch("value",function(b){a.val=b}),a.readonly=!1,b.readonly&&a.$parent.$watch(c(b.readonly),function(b){a.readonly=!!b})}]).directive("rating",function(){return{restrict:"EA",scope:{value:"=",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0}}),angular.module("mm.foundation.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];angular.isUndefined(a.openOnLoad)&&(a.openOnLoad=!0),b.select=function(a){angular.forEach(c,function(a){a.active=!1}),a.active=!0},b.addTab=function(d){c.push(d),a.openOnLoad&&(1===c.length||d.active)&&b.select(d)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{openOnLoad:"=?"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1,a.type=angular.isDefined(c.type)?a.$parent.$eval(c.type):"tabs"}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){var g,h;e.active?(g=a(e.active),h=g.assign,b.$parent.$watch(g,function(a,c){a!==c&&(b.active=!!a)}),b.active=g(b.$parent)):h=g=angular.noop,b.$watch("active",function(a){angular.isFunction(h)&&(h(b.$parent,a),a?(f.select(b),b.onSelect()):b.onDeselect())}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b,c,d){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("mm.foundation.topbar",["mm.foundation.mediaQueries"]).factory("closest",[function(){return function(a,b){for(var c=function(a,b){for(var c=(a.parentNode||a.document).querySelectorAll(b),d=-1;c[++d]&&c[d]!=a;);return!!c[d]},d=a[0];d;){if(c(d,b))return angular.element(d);d=d.parentElement}return!1}}]).directive("topBar",["$timeout","$compile","$window","$document","mediaQueries",function(a,b,c,d,e){return{scope:{stickyClass:"@",backText:"@",stickyOn:"=",customBackText:"=",isHover:"=",mobileShowParentLink:"=",scrolltop:"="},restrict:"EA",replace:!0,templateUrl:"template/topbar/top-bar.html",transclude:!0,controller:["$window","$scope","closest",function(b,c,f){c.settings={},c.settings.stickyClass=c.stickyClass||"sticky",c.settings.backText=c.backText||"Back",c.settings.stickyOn=c.stickyOn||"all",c.settings.customBackText=void 0===c.customBackText?!0:c.customBackText,c.settings.isHover=void 0===c.isHover?!0:c.isHover,c.settings.mobileShowParentLink=void 0===c.mobileShowParentLink?!0:c.mobileShowParentLink,c.settings.scrolltop=void 0===c.scrolltop?!0:c.scrolltop,this.settings=c.settings,c.index=0;var g=function(a){var b=a.offsetHeight,c=a.currentStyle||getComputedStyle(a);return b+=parseInt(c.marginTop,10)+parseInt(c.marginBottom,10)},h=[];this.addSection=function(a){h.push(a)},this.removeSection=function(a){var b=h.indexOf(a);b>-1&&h.splice(b,1)};var i=/rtl/i.test(d.find("html").attr("dir"))?"right":"left";c.$watch("index",function(a){for(var b=0;bb&&!h.hasClass("fixed")?(h.addClass("fixed"),i.css("padding-top",a.originalHeight+"px")):c.pageYOffset<=b&&h.hasClass("fixed")&&(h.removeClass("fixed"),i.css("padding-top",""))}},m=function(){var b=e.topbarBreakpoint();if(j!==b){j=e.topbarBreakpoint(),g.removeClass("expanded"),g.parent().removeClass("expanded"),a.height="";var c=angular.element(g[0].querySelectorAll("section"));angular.forEach(c,function(a){angular.element(a.querySelectorAll("li.moved")).removeClass("moved")}),a.$apply()}},n=function(){l(),a.$apply()};if(a.toggle=function(b){if(!e.topbarBreakpoint())return!1;var d=void 0===b?!g.hasClass("expanded"):b;d?g.addClass("expanded"):g.removeClass("expanded"),a.settings.scrolltop?!d&&g.hasClass("fixed")?(g.parent().addClass("fixed"),g.removeClass("fixed"),i.css("padding-top",a.originalHeight+"px")):d&&g.parent().hasClass("fixed")&&(g.parent().removeClass("fixed"),g.addClass("fixed"),i.css("padding-top",""),c.scrollTo(0,0)):(k()&&g.parent().addClass("fixed"),g.parent().hasClass("fixed")&&(d?(g.addClass("fixed"),g.parent().addClass("expanded"),i.css("padding-top",a.originalHeight+"px")):(g.removeClass("fixed"),g.parent().removeClass("expanded"),l())))},h.hasClass("fixed")||k()){a.stickyTopbar=!0,a.height=h[0].offsetHeight;var o=h[0].getBoundingClientRect().top+c.pageYOffset}else a.height=g[0].offsetHeight;a.originalHeight=a.height,a.$watch("height",function(a){a?g.css("height",a+"px"):g.css("height","")}),angular.element(c).bind("resize",m),angular.element(c).bind("scroll",n),a.$on("$destroy",function(){angular.element(c).unbind("resize",m),angular.element(c).unbind("scroll",n)}),h.hasClass("fixed")&&i.css("padding-top",a.originalHeight+"px")}}}]).directive("toggleTopBar",["closest",function(a){return{scope:{},require:"^topBar",restrict:"A",replace:!0,templateUrl:"template/topbar/toggle-top-bar.html",transclude:!0,link:function(b,c,d,e){c.bind("click",function(b){var c=a(angular.element(b.currentTarget),"li");c.hasClass("back")||c.hasClass("has-dropdown")||e.toggle()}),b.$on("$destroy",function(){c.unbind("click")})}}}]).directive("topBarSection",["$compile","closest",function(a,b){return{scope:{},require:"^topBar",restrict:"EA",replace:!0,templateUrl:"template/topbar/top-bar-section.html",transclude:!0,link:function(a,c,d,e){var f=c;a.reset=function(){angular.element(f[0].querySelectorAll("li.moved")).removeClass("moved")},a.move=function(a,b){"left"===a?f.css({left:-100*b+"%"}):f.css({right:-100*b+"%"})},e.addSection(a),a.$on("$destroy",function(){e.removeSection(a)});var g=f[0].querySelectorAll("li>a");angular.forEach(g,function(c){var d=angular.element(c),f=b(d,"li");f.hasClass("has-dropdown")||f.hasClass("back")||f.hasClass("title")||(d.bind("click",function(){e.toggle(!1)}),a.$on("$destroy",function(){d.bind("click")}))})}}}]).directive("hasDropdown",["mediaQueries",function(a){return{scope:{},require:"^topBar",restrict:"A",templateUrl:"template/topbar/has-dropdown.html",replace:!0,transclude:!0,link:function(b,c,d,e){b.triggerLink=c.children("a")[0];var f=angular.element(b.triggerLink);f.bind("click",function(a){e.forward(a)}),b.$on("$destroy",function(){f.unbind("click")}),c.bind("mouseenter",function(){e.settings.isHover&&!a.topbarBreakpoint()&&c.addClass("not-click")}),c.bind("click",function(b){e.settings.isHover||a.topbarBreakpoint()||c.toggleClass("not-click")}),c.bind("mouseleave",function(){c.removeClass("not-click")}),b.$on("$destroy",function(){c.unbind("click"),c.unbind("mouseenter"),c.unbind("mouseleave")})},controller:["$window","$scope",function(a,b){this.triggerLink=b.triggerLink}]}}]).directive("topBarDropdown",["$compile",function(a){return{scope:{},require:["^topBar","^hasDropdown"],restrict:"A",replace:!0,templateUrl:"template/topbar/top-bar-dropdown.html",transclude:!0,link:function(b,c,d,e){var f,g=e[0],h=e[1],i=angular.element(h.triggerLink),j=i.attr("href");b.linkText=i.text(),b.back=function(a){g.back(a)},g.settings.customBackText?b.backText=g.settings.backText:b.backText="« "+i.html(),f=g.settings.mobileShowParentLink&&j&&j.length>1?angular.element('
  • {{backText}}
  • {{linkText}}
  • '):angular.element('
  • {{backText}}
  • '),a(f)(b),c.prepend(f)}}}]),angular.module("mm.foundation.tour",["mm.foundation.position","mm.foundation.tooltip"]).service("$tour",["$window",function(a){function b(){try{return parseInt(a.localStorage.getItem("mm.tour.step"),10)}catch(b){if("SecurityError"!==b.name)throw b}}function c(){try{a.localStorage.setItem("mm.tour.step",e)}catch(b){if("SecurityError"!==b.name)throw b}}function d(a){e=a,c()}var e=b(),f={};this.add=function(a,b){f[a]=b},this.has=function(a){return!!f[a]},this.isActive=function(){return e>0},this.current=function(a){return a?void d(e):e},this.start=function(){d(1)},this.next=function(){d(e+1)},this.end=function(){d(0)}}]).directive("stepTextPopup",["$tour",function(a){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tour/tour.html",link:function(b,c){b.isLastStep=function(){return!a.has(a.current()+1)},b.endTour=function(){c.remove(),a.end()},b.nextStep=function(){c.remove(),a.next()},b.$on("$locationChangeSuccess",b.endTour)}}}]).directive("stepText",["$position","$tooltip","$tour","$window",function(a,b,c,d){function e(a){var b=a[0].getBoundingClientRect();return b.top>=0&&b.left>=0&&b.bottom<=d.innerHeight-80&&b.right<=d.innerWidth}function f(b,f,g){var h=parseInt(g.stepIndex,10);if(c.isActive()&&h&&(c.add(h,g),h===c.current())){if(!e(f)){var i=a.offset(f);d.scrollTo(0,i.top-d.innerHeight/2)}return!0}return!1}return b("stepText","step",f)}]),angular.module("mm.foundation.typeahead",["mm.foundation.position","mm.foundation.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+c+"'.");return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?b(k.typeaheadAppendToBody):!1,u=i.$eval(k.typeaheadFocusFirst)!==!1,v=b(k.ngModel).assign,w=g.parse(k.typeahead),x=angular.element("
    ");x.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&x.attr("template-url",k.typeaheadTemplateUrl);var y=i.$new();i.$on("$destroy",function(){y.$destroy()});var z=function(){y.matches=[],y.activeIdx=-1},A=function(a){var b={$viewValue:a};q(i,!0),c.when(w.source(i,b)).then(function(c){if(a===l.$viewValue&&m)if(c.length>0){y.activeIdx=u?0:-1,y.matches.length=0;for(var d=0;d=n?o>0?(B&&d.cancel(B),B=d(function(){A(a)},o)):A(a):(q(i,!1),z()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[w.itemName]=a,b=w.viewMapper(i,d),d[w.itemName]=void 0,c=w.viewMapper(i,d),b!==c?b:a)}),y.select=function(a){var b,c,d={};d[w.itemName]=c=y.matches[a].model,b=w.modelMapper(i,d),v(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:w.viewMapper(i,d)}),z(),j[0].focus()},j.bind("keydown",function(a){0!==y.matches.length&&-1!==h.indexOf(a.which)&&(-1!=y.activeIdx||13!==a.which&&9!==a.which)&&(a.preventDefault(),40===a.which?(y.activeIdx=(y.activeIdx+1)%y.matches.length,y.$digest()):38===a.which?(y.activeIdx=(y.activeIdx>0?y.activeIdx:y.matches.length)-1,y.$digest()):13===a.which||9===a.which?y.$apply(function(){y.select(y.activeIdx)}):27===a.which&&(a.stopPropagation(),z(),y.$digest()))}),j.bind("blur",function(a){m=!1}),j.bind("focus",function(a){m=!0});var C=function(a){j[0]!==a.target&&(z(),y.$digest())};e.bind("click",C),i.$on("$destroy",function(){e.unbind("click",C)});var D=a(x)(y);t?e.find("body").append(D):j.after(D)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).then(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?b.replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
    \n {{heading}}\n
    \n
    \n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
    \n')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html","
    \n \n ×\n
    \n")}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'
    \n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'
    \n
    \n
    \n')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'\n')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'\n')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'\n \n \n\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'\n \n \n\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
    \n \n
    \n

    \n

    \n
    \n
    \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'\n')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
    \n')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
    \n \n
    \n')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n\n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
    \n {{heading}}\n
    \n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/topbar/has-dropdown.html",[]).run(["$templateCache",function(a){a.put("template/topbar/has-dropdown.html",'
  • ')}]),angular.module("template/topbar/toggle-top-bar.html",[]).run(["$templateCache",function(a){a.put("template/topbar/toggle-top-bar.html",'')}]),angular.module("template/topbar/top-bar-dropdown.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar-dropdown.html",'')}]),angular.module("template/topbar/top-bar-section.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar-section.html",'
    ')}]),angular.module("template/topbar/top-bar.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar.html",'')}]),angular.module("template/tour/tour.html",[]).run(["$templateCache",function(a){a.put("template/tour/tour.html",'
    \n \n
    \n

    \n

    \n Next\n End\n ×\n
    \n
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html","
      \n"+'
    • \n
      \n
    • \n
    \n')}]); \ No newline at end of file diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index eed37bf99e..37194fc2b5 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -8,7 +8,7 @@ //= require lodash.underscore.js //= require angular-flash.min.js //= require shared/ng-tags-input.min.js -//= require shared/mm-foundation-tpls-0.8.0.min.js +//= require shared/mm-foundation-tpls-0.9.0-20180826174721.min.js //= require textAngular-rangy.min.js //= require textAngular-sanitize.min.js //= require textAngular.min.js From f03eb0226941d6cd5f902cc7bf72ce8a10ebbfc9 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Thu, 16 Aug 2018 00:19:36 +0800 Subject: [PATCH 031/190] Keep height of modal, dialog within screen height --- app/assets/stylesheets/darkswarm/modals.css.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/modals.css.scss b/app/assets/stylesheets/darkswarm/modals.css.scss index 300e769f01..30b66eeb2b 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.scss +++ b/app/assets/stylesheets/darkswarm/modals.css.scss @@ -21,9 +21,10 @@ dialog // Small - when modal IS full screen @media only screen and (max-width: 640px) { height: 500px; + left: 0; + max-height: 100%; position: absolute !important; top: 0; - left: 0; } // Medium and up - when modal IS NOT full screen From f4725198e8eeb9ea5a7bb38135102ac1bc60f2a0 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Mon, 20 Aug 2018 03:20:24 +0800 Subject: [PATCH 032/190] Move some CSS to pages/ subdirectory. --- app/assets/stylesheets/admin/all.scss | 4 +++- .../pages/product_image_upload_modal.css.scss | 18 ++++++++++++++++++ .../stylesheets/admin/products.css.scss | 19 ------------------- app/assets/stylesheets/darkswarm/all.scss | 3 ++- .../{ => pages}/cookies_banner.css.scss | 2 +- .../{ => pages}/cookies_policy_modal.css.scss | 2 +- .../login_modal.css.scss} | 0 7 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss rename app/assets/stylesheets/darkswarm/{ => pages}/cookies_banner.css.scss (95%) rename app/assets/stylesheets/darkswarm/{ => pages}/cookies_policy_modal.css.scss (94%) rename app/assets/stylesheets/darkswarm/{modal-login.css.scss => pages/login_modal.css.scss} (100%) diff --git a/app/assets/stylesheets/admin/all.scss b/app/assets/stylesheets/admin/all.scss index c6b7412c79..637f0b5d2c 100644 --- a/app/assets/stylesheets/admin/all.scss +++ b/app/assets/stylesheets/admin/all.scss @@ -15,4 +15,6 @@ */ @import 'variables'; -@import '**/*'; +@import 'components/*'; +@import '*'; +@import 'pages/*'; diff --git a/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss b/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss new file mode 100644 index 0000000000..ab57630438 --- /dev/null +++ b/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss @@ -0,0 +1,18 @@ +.reveal-modal.product-image-upload { + width: 300px; + a.close-reveal-modal { + font-size: 23px; + color: #de6060; + right: 0.45rem; + top: 0.35rem; + :hover { + color: #bf4545; + } + } + div.image-preview { + //float: left; + } + label { + margin-top: 20px; + } +} diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index 933f43a00b..30cf3b5558 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -98,25 +98,6 @@ table#listing_products.bulk { } } -.reveal-modal.product-image-upload { - width: 300px; - a.close-reveal-modal { - font-size: 23px; - color: #de6060; - right: 0.45rem; - top: 0.35rem; - :hover { - color: #bf4545; - } - } - div.image-preview { - //float: left; - } - label { - margin-top: 20px; - } -} - form#image_upload { text-align: center; .spinner { diff --git a/app/assets/stylesheets/darkswarm/all.scss b/app/assets/stylesheets/darkswarm/all.scss index 6cfc32e605..7c27103b17 100644 --- a/app/assets/stylesheets/darkswarm/all.scss +++ b/app/assets/stylesheets/darkswarm/all.scss @@ -10,7 +10,8 @@ @import 'foundation-icons'; @import '*'; +@import 'pages/*'; ofn-modal { display: block; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/darkswarm/cookies_banner.css.scss b/app/assets/stylesheets/darkswarm/pages/cookies_banner.css.scss similarity index 95% rename from app/assets/stylesheets/darkswarm/cookies_banner.css.scss rename to app/assets/stylesheets/darkswarm/pages/cookies_banner.css.scss index 723b41eb1f..6223220977 100644 --- a/app/assets/stylesheets/darkswarm/cookies_banner.css.scss +++ b/app/assets/stylesheets/darkswarm/pages/cookies_banner.css.scss @@ -1,4 +1,4 @@ -@import 'branding'; +@import '../branding'; .cookies-banner { background: $dark-grey; diff --git a/app/assets/stylesheets/darkswarm/cookies_policy_modal.css.scss b/app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss similarity index 94% rename from app/assets/stylesheets/darkswarm/cookies_policy_modal.css.scss rename to app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss index 029cf1f211..942a4396e0 100644 --- a/app/assets/stylesheets/darkswarm/cookies_policy_modal.css.scss +++ b/app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss @@ -1,4 +1,4 @@ -@import 'branding'; +@import '../branding'; .cookies-policy-modal { background: $disabled-light; diff --git a/app/assets/stylesheets/darkswarm/modal-login.css.scss b/app/assets/stylesheets/darkswarm/pages/login_modal.css.scss similarity index 100% rename from app/assets/stylesheets/darkswarm/modal-login.css.scss rename to app/assets/stylesheets/darkswarm/pages/login_modal.css.scss From 169aa752d8290a6d4363f82e6cc7579369f2f5b9 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sun, 26 Aug 2018 22:01:24 +0800 Subject: [PATCH 033/190] Enlarge potential size of modals for large screens Change modals for large screens from 10% position from top and 80% max height, to 5% position from top and 90% max height. This is to accommodate the taller cookies policy modal. It seems that it is not easy to apply a custom position and height to a specific modal only. This doesn't seem to be supported by the modal library currently being used. Before the recent changes, most modals were rendering at 10px top position (not the originally planned 10%), so changing this to 5% doesn't seem to be much of a compromise. --- app/assets/stylesheets/darkswarm/animations.scss | 6 +++--- app/assets/stylesheets/darkswarm/modals.css.scss | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/animations.scss b/app/assets/stylesheets/darkswarm/animations.scss index 8a6efdc161..b3848777ba 100644 --- a/app/assets/stylesheets/darkswarm/animations.scss +++ b/app/assets/stylesheets/darkswarm/animations.scss @@ -125,9 +125,9 @@ transition: transform 0.2s ease-out; @media only screen and (min-width: 641px) { - -webkit-transform: translate(0, -10%); - -ms-transform: translate(0, -10%); - transform: translate(0, -10%); + -webkit-transform: translate(0, -5%); + -ms-transform: translate(0, -5%); + transform: translate(0, -5%); } } diff --git a/app/assets/stylesheets/darkswarm/modals.css.scss b/app/assets/stylesheets/darkswarm/modals.css.scss index 30b66eeb2b..3358848aca 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.scss +++ b/app/assets/stylesheets/darkswarm/modals.css.scss @@ -29,8 +29,8 @@ dialog // Medium and up - when modal IS NOT full screen @media only screen and (min-width: 641px) { - top: 10%; - max-height: 80%; + top: 5%; + max-height: 90%; } } From 9258e8c464c8550185f0367c8ad9efc11af71e0c Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sun, 26 Aug 2018 22:22:40 +0800 Subject: [PATCH 034/190] Remove height limit for modals in small screens --- app/assets/stylesheets/darkswarm/modals.css.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/darkswarm/modals.css.scss b/app/assets/stylesheets/darkswarm/modals.css.scss index 3358848aca..ed90ba7004 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.scss +++ b/app/assets/stylesheets/darkswarm/modals.css.scss @@ -20,7 +20,6 @@ dialog // Small - when modal IS full screen @media only screen and (max-width: 640px) { - height: 500px; left: 0; max-height: 100%; position: absolute !important; From 339ea6fa91915031d2407426df45b668c4f2a8bb Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Mon, 27 Aug 2018 03:25:09 +0800 Subject: [PATCH 035/190] Clean up SCSS as suggested by scss-lint --- .../pages/product_image_upload_modal.css.scss | 18 ++++++++------- .../stylesheets/admin/variables.css.scss | 3 +++ app/assets/stylesheets/darkswarm/all.scss | 1 + .../darkswarm/base/colors.css.scss | 4 ++++ .../stylesheets/darkswarm/modals.css.scss | 2 +- .../darkswarm/pages/login_modal.css.scss | 22 +++++++++---------- 6 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 app/assets/stylesheets/darkswarm/base/colors.css.scss diff --git a/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss b/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss index ab57630438..4dffade73e 100644 --- a/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss +++ b/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss @@ -1,17 +1,19 @@ +@import '../variables'; + .reveal-modal.product-image-upload { width: 300px; - a.close-reveal-modal { + + .close-reveal-modal { + color: $modal-close-button-color; font-size: 23px; - color: #de6060; - right: 0.45rem; - top: 0.35rem; + right: .45rem; + top: .35rem; + :hover { - color: #bf4545; + color: $modal-close-button-hover-color; } } - div.image-preview { - //float: left; - } + label { margin-top: 20px; } diff --git a/app/assets/stylesheets/admin/variables.css.scss b/app/assets/stylesheets/admin/variables.css.scss index c7f904a38a..f8c12e0d4d 100644 --- a/app/assets/stylesheets/admin/variables.css.scss +++ b/app/assets/stylesheets/admin/variables.css.scss @@ -10,3 +10,6 @@ $medium-grey: #919191; $pale-blue: #cee1f4; $admin-table-border: $pale-blue; + +$modal-close-button-color: #de6060; +$modal-close-button-hover-color: #bf4545; diff --git a/app/assets/stylesheets/darkswarm/all.scss b/app/assets/stylesheets/darkswarm/all.scss index 7c27103b17..b3f5433d8e 100644 --- a/app/assets/stylesheets/darkswarm/all.scss +++ b/app/assets/stylesheets/darkswarm/all.scss @@ -9,6 +9,7 @@ @import 'foundation'; @import 'foundation-icons'; +@import 'base/*'; @import '*'; @import 'pages/*'; diff --git a/app/assets/stylesheets/darkswarm/base/colors.css.scss b/app/assets/stylesheets/darkswarm/base/colors.css.scss new file mode 100644 index 0000000000..4b36b9cb9b --- /dev/null +++ b/app/assets/stylesheets/darkswarm/base/colors.css.scss @@ -0,0 +1,4 @@ +$modal-background-color: #efefef; +$modal-content-background-color: #fff; +$modal-alert-link-color: #fff; +$modal-alert-link-hover-color: rgba(255, 255, 255, .7); diff --git a/app/assets/stylesheets/darkswarm/modals.css.scss b/app/assets/stylesheets/darkswarm/modals.css.scss index ed90ba7004..734d388acf 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.scss +++ b/app/assets/stylesheets/darkswarm/modals.css.scss @@ -28,8 +28,8 @@ dialog // Medium and up - when modal IS NOT full screen @media only screen and (min-width: 641px) { - top: 5%; max-height: 90%; + top: 5%; } } diff --git a/app/assets/stylesheets/darkswarm/pages/login_modal.css.scss b/app/assets/stylesheets/darkswarm/pages/login_modal.css.scss index 80687c645f..3a9d385441 100644 --- a/app/assets/stylesheets/darkswarm/pages/login_modal.css.scss +++ b/app/assets/stylesheets/darkswarm/pages/login_modal.css.scss @@ -1,25 +1,25 @@ +@import '../base/colors'; + // Styling for login modal to style tabs .reveal-modal.login-modal { - border-bottom-color: #efefef; + border-bottom-color: $modal-background-color; } .login-modal { - background: #efefef; + background: $modal-background-color; .tabs-content { - background: #fff; + background: $modal-content-background-color; padding-top: 10px; } - .alert-box { - a { - color: white; - text-decoration: underline; + .alert-box a { + color: $modal-alert-link-color; + text-decoration: underline; - &:hover { - color: rgba(255, 255, 255, 0.7); - text-decoration: underline; - } + &:hover { + color: $modal-alert-link-hover-color; + text-decoration: underline; } } } From 28d2bb3d47d59fa1bd92652812453ed4f464f9ab Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sat, 1 Sep 2018 20:36:22 +0800 Subject: [PATCH 036/190] Wrap controller specs for viewing cart --- .../spree/orders_controller_spec.rb | 106 +++++++++--------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index b9012ff890..58957ede34 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -5,61 +5,63 @@ describe Spree::OrdersController, type: :controller do let(:order) { create(:order) } let(:order_cycle) { create(:simple_order_cycle) } - it "redirects home when no distributor is selected" do - spree_get :edit - expect(response).to redirect_to root_path - end - - it "redirects to shop when order is empty" do - allow(controller).to receive(:current_distributor).and_return(distributor) - allow(controller).to receive(:current_order_cycle).and_return(order_cycle) - allow(controller).to receive(:current_order).and_return order - allow(order).to receive_message_chain(:line_items, :empty?).and_return true - allow(order).to receive(:insufficient_stock_lines).and_return [] - session[:access_token] = order.token - spree_get :edit - expect(response).to redirect_to shop_path - end - - it "redirects to the shop when no order cycle is selected" do - allow(controller).to receive(:current_distributor).and_return(distributor) - spree_get :edit - expect(response).to redirect_to shop_path - end - - it "redirects home with message if hub is not ready for checkout" do - allow(VariantOverride).to receive(:indexed).and_return({}) - - order = subject.current_order(true) - allow(distributor).to receive(:ready_for_checkout?) { false } - allow(order).to receive_messages(distributor: distributor, order_cycle: order_cycle) - - expect(order).to receive(:empty!) - expect(order).to receive(:set_distribution!).with(nil, nil) - - spree_get :edit - - expect(response).to redirect_to root_url - expect(flash[:info]).to eq("The hub you have selected is temporarily closed for orders. Please try again later.") - end - - describe "when an item has insufficient stock" do - let(:order) { subject.current_order(true) } - let(:oc) { create(:simple_order_cycle, distributors: [d], variants: [variant]) } - let(:d) { create(:distributor_enterprise, shipping_methods: [create(:shipping_method)], payment_methods: [create(:payment_method)]) } - let(:variant) { create(:variant, on_demand: false, on_hand: 5) } - let(:line_item) { order.line_items.last } - - before do - order.set_distribution! d, oc - order.add_variant variant, 5 - variant.update_attributes! on_hand: 3 + describe "viewing cart" do + it "redirects home when no distributor is selected" do + spree_get :edit + expect(response).to redirect_to root_path end - it "displays a flash message when we view the cart" do + it "redirects to shop when order is empty" do + allow(controller).to receive(:current_distributor).and_return(distributor) + allow(controller).to receive(:current_order_cycle).and_return(order_cycle) + allow(controller).to receive(:current_order).and_return order + allow(order).to receive_message_chain(:line_items, :empty?).and_return true + allow(order).to receive(:insufficient_stock_lines).and_return [] + session[:access_token] = order.token spree_get :edit - expect(response.status).to eq 200 - expect(flash[:error]).to eq("An item in your cart has become unavailable.") + expect(response).to redirect_to shop_path + end + + it "redirects to the shop when no order cycle is selected" do + allow(controller).to receive(:current_distributor).and_return(distributor) + spree_get :edit + expect(response).to redirect_to shop_path + end + + it "redirects home with message if hub is not ready for checkout" do + allow(VariantOverride).to receive(:indexed).and_return({}) + + order = subject.current_order(true) + allow(distributor).to receive(:ready_for_checkout?) { false } + allow(order).to receive_messages(distributor: distributor, order_cycle: order_cycle) + + expect(order).to receive(:empty!) + expect(order).to receive(:set_distribution!).with(nil, nil) + + spree_get :edit + + expect(response).to redirect_to root_url + expect(flash[:info]).to eq("The hub you have selected is temporarily closed for orders. Please try again later.") + end + + describe "when an item has insufficient stock" do + let(:order) { subject.current_order(true) } + let(:oc) { create(:simple_order_cycle, distributors: [d], variants: [variant]) } + let(:d) { create(:distributor_enterprise, shipping_methods: [create(:shipping_method)], payment_methods: [create(:payment_method)]) } + let(:variant) { create(:variant, on_demand: false, on_hand: 5) } + let(:line_item) { order.line_items.last } + + before do + order.set_distribution! d, oc + order.add_variant variant, 5 + variant.update_attributes! on_hand: 3 + end + + it "displays a flash message when we view the cart" do + spree_get :edit + expect(response.status).to eq 200 + expect(flash[:error]).to eq("An item in your cart has become unavailable.") + end end end From 3bf9e9551100cd0b96ee8fcdb7ff36b229effc59 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sun, 2 Sep 2018 03:06:30 +0800 Subject: [PATCH 037/190] Add specs for existing behaviour in order page --- .../spree/orders_controller_spec.rb | 65 ++++++++++++++++ .../features/consumer/shopping/orders_spec.rb | 77 +++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 58957ede34..43b26a17b7 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -5,6 +5,71 @@ describe Spree::OrdersController, type: :controller do let(:order) { create(:order) } let(:order_cycle) { create(:simple_order_cycle) } + describe "viewing an order" do + let(:customer) { create(:customer) } + let(:order) { create(:order_with_credit_payment, customer: customer, distributor: customer.enterprise) } + + before do + allow(controller).to receive(:spree_current_user) { current_user } + end + + context "after checking out as an anonymous guest" do + let(:customer) { create(:customer, user: nil) } + let(:current_user) { nil } + + it "loads page" do + spree_get :show, id: order.number, token: order.token + expect(response).to be_success + end + + it "stores order token in session as 'access_token'" do + spree_get :show, id: order.number, token: order.token + expect(session[:access_token]).to eq(order.token) + end + end + + context "when returning to order page after checking out as an anonymous guest" do + let(:customer) { create(:customer, user: nil) } + let(:current_user) { nil } + + before do + session[:access_token] = order.token + end + + it "loads page" do + spree_get :show, id: order.number + expect(response).to be_success + end + end + + context "when logged in as the customer" do + let(:current_user) { order.user } + + it "loads page" do + spree_get :show, id: order.number + expect(response).to be_success + end + end + + context "when logged in as another customer" do + let(:current_user) { create(:user) } + + it "redirects to unauthorized" do + spree_get :show, id: order.number + expect(response.status).to eq(401) + end + end + + context "when neither checked out as an anonymous guest nor logged in" do + let(:current_user) { nil } + + it "redirects to unauthorized" do + spree_get :show, id: order.number + expect(response.status).to eq(401) + end + end + end + describe "viewing cart" do it "redirects home when no distributor is selected" do spree_get :edit diff --git a/spec/features/consumer/shopping/orders_spec.rb b/spec/features/consumer/shopping/orders_spec.rb index ada9ba8020..f4b78ff752 100644 --- a/spec/features/consumer/shopping/orders_spec.rb +++ b/spec/features/consumer/shopping/orders_spec.rb @@ -3,6 +3,79 @@ require 'spec_helper' feature "Order Management", js: true do include AuthenticationWorkflow + describe "viewing a completed order" do + let!(:distributor) { create(:distributor_enterprise) } + let!(:customer) { create(:customer, user: user, enterprise: distributor) } + let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } + + let!(:bill_address) { create(:address) } + let!(:ship_address) { create(:address) } + let!(:shipping_method) { create(:free_shipping_method, distributors: [distributor]) } + + let!(:order) do + create(:order_with_credit_payment, + customer: customer, + user: user, + distributor: distributor, + order_cycle: order_cycle + ) + end + + before do + # For some reason, both bill_address and ship_address are not set + # automatically. + # + # Also, assigning the shipping_method to a ShippingMethod instance results + # in a SystemStackError. + order.update_attributes!( + bill_address: bill_address, + ship_address: ship_address, + shipping_method_id: shipping_method.id + ) + end + + context "when checking out as an anonymous guest" do + let(:user) { nil } + + it "allows the user to see the details" do + # Cannot load the page without token + visit spree.order_path(order) + expect(page).to_not be_confirmed_order_page + + # Can load the page with token + visit spree.order_path(order, token: order.token) + expect(page).to be_confirmed_order_page + + # Can load the page even without the token, after loading the page with + # token. + visit spree.order_path(order) + expect(page).to be_confirmed_order_page + end + end + + context "when logged in as the customer" do + let(:user) { create(:user) } + + before do + login_as user + end + + it "allows the user to see order details" do + visit spree.order_path(order) + expect(page).to be_confirmed_order_page + end + end + + context "when not logged in" do + let(:user) { create(:user) } + + it "does not allow the user to see order details" do + visit spree.order_path(order) + expect(page).to_not be_confirmed_order_page + end + end + end + describe "editing a completed order" do let(:address) { create(:address) } let(:user) { create(:user, bill_address: address, ship_address: address) } @@ -86,4 +159,8 @@ feature "Order Management", js: true do end end end + + def be_confirmed_order_page + have_content /Order #\w+ Confirmed NOT PAID/ + end end From cd41498da9682d35ff8030097fc69f208cdb560d Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sun, 2 Sep 2018 04:03:42 +0800 Subject: [PATCH 038/190] Ask to login when not authenticated for order page Redirect the user to the login page, instead of responding with HTTP 401. --- app/controllers/spree/orders_controller_decorator.rb | 8 ++++++++ config/locales/en.yml | 2 ++ spec/controllers/spree/orders_controller_spec.rb | 7 ++++++- spec/features/consumer/shopping/orders_spec.rb | 7 ++++++- spec/support/request/authentication_workflow.rb | 10 +++++++--- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index a354852391..72b5cf3c14 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -5,6 +5,7 @@ Spree::OrdersController.class_eval do before_filter :filter_order_params, only: :update before_filter :enable_embedded_shopfront + prepend_before_filter :require_order_authentication, only: :show prepend_before_filter :require_order_cycle, only: :edit prepend_before_filter :require_distributor_chosen, only: :edit before_filter :check_hub_ready_for_checkout, only: :edit @@ -128,6 +129,13 @@ Spree::OrdersController.class_eval do private + def require_order_authentication + return if session[:access_token] || params[:token] || spree_current_user + + flash[:error] = I18n.t("spree.orders.edit.login_to_view_order") + redirect_to root_path(anchor: "login?after_login=#{request.env['PATH_INFO']}") + end + def order_to_update return @order_to_update if defined? @order_to_update return @order_to_update = current_order unless params[:id] diff --git a/config/locales/en.yml b/config/locales/en.yml index f34706ca29..7d3ce3b831 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2699,6 +2699,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using inventory: Inventory zipcode: Postcode orders: + edit: + login_to_view_order: "Please log in to view your order." bought: item: "Already ordered in this order cycle" shipment_states: diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 43b26a17b7..d2f2cbf8da 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -63,9 +63,14 @@ describe Spree::OrdersController, type: :controller do context "when neither checked out as an anonymous guest nor logged in" do let(:current_user) { nil } + before do + request.env["PATH_INFO"] = spree.order_path(order) + end + it "redirects to unauthorized" do spree_get :show, id: order.number - expect(response.status).to eq(401) + expect(response).to redirect_to(root_path(anchor: "login?after_login=#{spree.order_path(order)}")) + expect(flash[:error]).to eq("Please log in to view your order.") end end end diff --git a/spec/features/consumer/shopping/orders_spec.rb b/spec/features/consumer/shopping/orders_spec.rb index f4b78ff752..4f0adfe772 100644 --- a/spec/features/consumer/shopping/orders_spec.rb +++ b/spec/features/consumer/shopping/orders_spec.rb @@ -69,9 +69,14 @@ feature "Order Management", js: true do context "when not logged in" do let(:user) { create(:user) } - it "does not allow the user to see order details" do + it "allows the user to see order details after login" do + # Cannot load the page without signing in visit spree.order_path(order) expect(page).to_not be_confirmed_order_page + + # Can load the page after signing in + fill_in_and_submit_login_form user + expect(page).to be_confirmed_order_page end end end diff --git a/spec/support/request/authentication_workflow.rb b/spec/support/request/authentication_workflow.rb index 5d3181ea4f..7645db0012 100644 --- a/spec/support/request/authentication_workflow.rb +++ b/spec/support/request/authentication_workflow.rb @@ -54,9 +54,13 @@ module AuthenticationWorkflow user.spree_roles << user_role visit spree.login_path - fill_in 'email', :with => 'someone@ofn.org' - fill_in 'password', :with => 'passw0rd' - click_button 'Login' + fill_in_and_submit_login_form user + end + + def fill_in_and_submit_login_form(user) + fill_in "email", with: user.email + fill_in "password", with: user.password + click_button "Login" end end From f9533f8fb80529a6b58ea2920999fe90a156834f Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Thu, 6 Sep 2018 15:14:41 +0800 Subject: [PATCH 039/190] Refactor redirecting back after requiring login --- app/controllers/spree/orders_controller_decorator.rb | 3 ++- lib/spree/core/controller_helpers/auth_decorator.rb | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 lib/spree/core/controller_helpers/auth_decorator.rb diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index 72b5cf3c14..1ad8853bb0 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -1,4 +1,5 @@ require 'spree/core/controller_helpers/order_decorator' +require 'spree/core/controller_helpers/auth_decorator' Spree::OrdersController.class_eval do before_filter :update_distribution, only: :update @@ -133,7 +134,7 @@ Spree::OrdersController.class_eval do return if session[:access_token] || params[:token] || spree_current_user flash[:error] = I18n.t("spree.orders.edit.login_to_view_order") - redirect_to root_path(anchor: "login?after_login=#{request.env['PATH_INFO']}") + require_login_then_redirect_to request.env['PATH_INFO'] end def order_to_update diff --git a/lib/spree/core/controller_helpers/auth_decorator.rb b/lib/spree/core/controller_helpers/auth_decorator.rb new file mode 100644 index 0000000000..1c5f9de911 --- /dev/null +++ b/lib/spree/core/controller_helpers/auth_decorator.rb @@ -0,0 +1,5 @@ +Spree::Core::ControllerHelpers::Auth.class_eval do + def require_login_then_redirect_to(url) + redirect_to root_path(anchor: "login?after_login=#{url}") + end +end From 6d5f8553efc6b1a9e35adda219bef1cd5ec11222 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Thu, 6 Sep 2018 18:08:42 +0800 Subject: [PATCH 040/190] Add "more" translation key This is used in places such as the adaptive menu in the admin section. This is actually included in spree_i18n but not correctly translated for fr. --- config/locales/en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index f34706ca29..ad9fff2782 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -162,6 +162,7 @@ en: free_trial: "free trial" plus_tax: "plus GST" min_bill_turnover_desc: "once turnover exceeds %{mbt_amount}" + more: "More" say_no: "No" say_yes: "Yes" then: then From d06bccb832e27b9228252f0838a42f82b14ad127 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Thu, 6 Sep 2018 17:14:23 +0800 Subject: [PATCH 041/190] Change i18n key for Store link in admin header spree_i18n translates the "store" key in different ways, sometimes like "shop" and sometimes like "save". This could be because of a clash in usage between Spree add-ons. To be more specific, the "Store" link in the admin section now uses the "admin.header.store" translation key. --- app/views/spree/layouts/admin/_login_nav.html.haml | 2 +- config/locales/en.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/spree/layouts/admin/_login_nav.html.haml b/app/views/spree/layouts/admin/_login_nav.html.haml index 088ac02377..816eab7bda 100644 --- a/app/views/spree/layouts/admin/_login_nav.html.haml +++ b/app/views/spree/layouts/admin/_login_nav.html.haml @@ -11,4 +11,4 @@ = link_to t(:logout), spree.logout_path %li{"data-hook" => "store-frontend-link"} %i.icon-external-link - = link_to t(:store), spree.root_path, :target => '_blank' + = link_to t(".header.store"), spree.root_path, target: "_blank" diff --git a/config/locales/en.yml b/config/locales/en.yml index f34706ca29..0f6e62a65f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2589,6 +2589,10 @@ See the %{link} to find out more about %{sitename}'s features and to start using my_account: "My account" date: "Date" time: "Time" + layouts: + admin: + header: + store: Store admin: orders: invoice: From b9fe1ced16b3fd07061bf737aea3f3583059f0c6 Mon Sep 17 00:00:00 2001 From: Frank West Date: Tue, 3 Jul 2018 13:39:59 -0700 Subject: [PATCH 042/190] Replace therubyracer with mini_racer This is being replaced to increase the reliability and speed of asset compilation. --- Gemfile | 3 +-- Gemfile.lock | 10 ++++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 257f9cf14b..c70fe0e668 100644 --- a/Gemfile +++ b/Gemfile @@ -87,8 +87,7 @@ group :assets do gem 'compass-rails' gem 'coffee-rails', '~> 3.2.1' - # See https://github.com/sstephenson/execjs#readme for more supported runtimes - gem 'therubyracer' + gem 'mini_racer' gem 'uglifier', '>= 1.0.3' diff --git a/Gemfile.lock b/Gemfile.lock index f4399c51c3..b79333c8eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -467,7 +467,7 @@ GEM addressable (~> 2.3) letter_opener (1.6.0) launchy (~> 2.2) - libv8 (3.16.14.19) + libv8 (6.3.292.48.1) listen (3.0.8) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -479,6 +479,8 @@ GEM mime-types (1.25.1) mini_mime (1.0.1) mini_portile2 (2.1.0) + mini_racer (0.1.15) + libv8 (~> 6.3) momentjs-rails (2.5.1) railties (>= 3.1) money (5.1.0) @@ -576,7 +578,6 @@ GEM rdoc (3.12.2) json (~> 1.4) redcarpet (3.2.3) - ref (2.0.0) representative (1.0.5) activesupport (>= 2.2.2) builder (>= 2.1.2) @@ -657,9 +658,6 @@ GEM stringex (1.3.3) stripe (3.3.2) faraday (~> 0.9) - therubyracer (0.12.3) - libv8 (~> 3.16.14.15) - ref thor (0.20.0) tilt (1.4.1) timecop (0.9.1) @@ -760,6 +758,7 @@ DEPENDENCIES knapsack letter_opener (>= 1.4.1) listen (= 3.0.8) + mini_racer momentjs-rails nokogiri (>= 1.6.7.1) oauth2 (~> 1.2.0) @@ -795,7 +794,6 @@ DEPENDENCIES spree_i18n! spree_paypal_express! stripe (~> 3.3.2) - therubyracer timecop truncate_html turbo-sprockets-rails3 From d6c2a364516738af47643578d371576db2ac07b6 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 7 Sep 2018 13:50:38 +1000 Subject: [PATCH 043/190] Blacklist libv8 version that breaks mini_racer --- Gemfile | 3 +++ Gemfile.lock | 1 + 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index c70fe0e668..a30ceeb59b 100644 --- a/Gemfile +++ b/Gemfile @@ -88,6 +88,9 @@ group :assets do gem 'coffee-rails', '~> 3.2.1' gem 'mini_racer' + # We found that the following version of libv8 breaks the compilation of mini_racer. + # Nothing else depends on libv8. + gem 'libv8', '!= 6.7.288.46.1' gem 'uglifier', '>= 1.0.3' diff --git a/Gemfile.lock b/Gemfile.lock index b79333c8eb..cf05ee57c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -757,6 +757,7 @@ DEPENDENCIES jwt (~> 1.5) knapsack letter_opener (>= 1.4.1) + libv8 (!= 6.7.288.46.1) listen (= 3.0.8) mini_racer momentjs-rails From f1d5088aaa3a44756b7741d7a6f978fef81d1a0c Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Fri, 7 Sep 2018 17:01:43 +0800 Subject: [PATCH 044/190] Add translations for order cancellation emails --- config/locales/en.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index f34706ca29..20d6e7a055 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -95,6 +95,14 @@ en: subject: "%{enterprise} is now on %{sitename}" invite_manager: subject: "%{enterprise} has invited you to be a manager" + order_mailer: + cancel_email: + dear_customer: "Dear Customer," + instructions: "Your order has been CANCELED. Please retain this cancellation information for your records." + order_summary_canceled: "Order Summary [CANCELED]" + subject: "Cancellation of Order" + subtotal: "Subtotal: %{subtotal}" + total: "Order Total: %{total}" producer_mailer: order_cycle: subject: "Order cycle report for %{producer}" From 2cf608ffdf72b47f3e8a04131be373583e43f6bb Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Fri, 7 Sep 2018 17:08:03 +0800 Subject: [PATCH 045/190] Add translations for shipment emails --- config/locales/en.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 20d6e7a055..31c4a5108e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -106,6 +106,15 @@ en: producer_mailer: order_cycle: subject: "Order cycle report for %{producer}" + shipment_mailer: + shipped_email: + dear_customer: "Dear Customer," + instructions: "Your order has been shipped" + shipment_summary: "Shipment Summary" + subject: "Shipment Notification" + thanks: "Thank you for your business." + track_information: "Tracking Information: %{tracking}" + track_link: "Tracking Link: %{url}" subscription_mailer: placement_summary_email: subject: A summary of recently placed subscription orders From 40443b19f176d9cf4cfa8fbbad2f40d698d4b55a Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Fri, 7 Sep 2018 17:50:27 +0800 Subject: [PATCH 046/190] Add translations for headers in enterprise index This is for the view when logged in as enterprise manager. The translations added are: * Producer? * Package * Status --- config/locales/en.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index f34706ca29..bdf68fadfa 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -821,6 +821,9 @@ en: search_placeholder: Search By Name manage: Manage manage_link: Settings + producer?: "Producer?" + package: "Package" + status: "Status" new_form: owner: Owner owner_tip: The primary user responsible for this enterprise. From 86f4a118dd1507c6959d8332f0b14554182ee4b1 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Fri, 7 Sep 2018 17:58:34 +0800 Subject: [PATCH 047/190] Translate placeholder for order cycle name filter --- app/views/admin/order_cycles/_filters.html.haml | 2 +- config/locales/en.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/admin/order_cycles/_filters.html.haml b/app/views/admin/order_cycles/_filters.html.haml index 4f0df00455..dad49a5232 100644 --- a/app/views/admin/order_cycles/_filters.html.haml +++ b/app/views/admin/order_cycles/_filters.html.haml @@ -2,7 +2,7 @@ .filter.four.columns.alpha %label{ :for => 'query' }=t('admin.quick_search') %br - %input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query' }, placeholder: "Search by Order Cycle name..." } + %input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query' }, placeholder: t(".search_by_order_cycle_name") } .filter_select.four.columns %label{ :for => 'involving_filter' }=t('.involving') %br diff --git a/config/locales/en.yml b/config/locales/en.yml index bdf68fadfa..a8b7243eb1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -867,6 +867,8 @@ en: save_reload: Save and Reload Page coordinator_fees: add: Add coordinator fee + filters: + search_by_order_cycle_name: "Search by Order Cycle name..." form: incoming: Incoming supplier: Supplier From c64acf47ab0efc743b9da91334efb33c32908feb Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Fri, 7 Sep 2018 18:08:20 +0800 Subject: [PATCH 048/190] Fix translation key for "involving" in OC filters --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index a8b7243eb1..2eb472b652 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -869,6 +869,7 @@ en: add: Add coordinator fee filters: search_by_order_cycle_name: "Search by Order Cycle name..." + involving: "Involving" form: incoming: Incoming supplier: Supplier @@ -883,7 +884,6 @@ en: delivery_details: Pickup / Delivery details debug_info: Debug information index: - involving: Involving schedule: Schedule schedules: Schedules adding_a_new_schedule: Adding A New Schedule From fc79aae17c868c47cbb39ce69cd2b78735f425e5 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Fri, 7 Sep 2018 18:18:34 +0800 Subject: [PATCH 049/190] Translate authorized shops headers in account page --- app/views/spree/users/_authorised_shops.html.haml | 4 ++-- config/locales/en.yml | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/spree/users/_authorised_shops.html.haml b/app/views/spree/users/_authorised_shops.html.haml index 692c953f12..a1cdd58a91 100644 --- a/app/views/spree/users/_authorised_shops.html.haml +++ b/app/views/spree/users/_authorised_shops.html.haml @@ -1,7 +1,7 @@ %table %tr - %th= t(:shop_title) - %th= t(:allow_charges?) + %th= t(".shop_name") + %th= t(".allow_charges?") %tr.customer{ id: "customer{{ customer.id }}", ng: { repeat: "customer in customers" } } %td.shop{ ng: { bind: 'shopsByID[customer.enterprise_id].name' } } %td.allow_charges diff --git a/config/locales/en.yml b/config/locales/en.yml index 2eb472b652..217fdbb687 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2819,5 +2819,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using authorised_shops: Authorised Shops authorised_shops_popover: This is the list of shops which are permitted to charge your default credit card for any subscriptions (ie. repeating orders) you may have. Your card details will be kept secure and will not be shared with shop owners. You will always be notified when you are charged. saved_cards_popover: This is the list of cards you have opted to save for later use. Your 'default' will be selected automatically when you checkout an order, and can be charged by any shops you have allowed to do so (see right). + authorised_shops: + shop_name: "Shop Name" + allow_charges?: "Allow Charges?" localized_number: invalid_format: has an invalid format. Please enter a number. From 9dcdb67352760090fd87927ed1508056fd68a010 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sat, 8 Sep 2018 01:37:51 +0800 Subject: [PATCH 050/190] Translate inherits_properties label in product form --- .../add_producer_properties_warning_and_table.html.haml.deface | 2 +- config/locales/en.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/overrides/spree/admin/product_properties/index/add_producer_properties_warning_and_table.html.haml.deface b/app/overrides/spree/admin/product_properties/index/add_producer_properties_warning_and_table.html.haml.deface index c31a74b9e4..149f72015a 100644 --- a/app/overrides/spree/admin/product_properties/index/add_producer_properties_warning_and_table.html.haml.deface +++ b/app/overrides/spree/admin/product_properties/index/add_producer_properties_warning_and_table.html.haml.deface @@ -1,7 +1,7 @@ / insert_after 'table.index.sortable' =f.check_box :inherits_properties -=f.label :inherits_properties, "Inherit properties from #{@product.supplier.name}? (unless overridden above)" +=f.label :inherits_properties, t(".inherits_properties_checkbox_hint", supplier: @product.supplier.name) %br %br diff --git a/config/locales/en.yml b/config/locales/en.yml index 217fdbb687..b06fdb80dc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2595,6 +2595,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using date: "Date" time: "Time" admin: + product_properties: + index: + inherits_properties_checkbox_hint: "Inherit properties from %{supplier}? (unless overridden above)" orders: invoice: issued_on: Issued on From 2c2dd62f6aaea3756c7cf756b5dfad1a40e6dd3e Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sat, 8 Sep 2018 02:06:52 +0800 Subject: [PATCH 051/190] Translate "COPY OF" when cloning an OC --- app/models/order_cycle.rb | 2 +- config/locales/en.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 4d8f443a68..a823d1fa4a 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -121,7 +121,7 @@ class OrderCycle < ActiveRecord::Base def clone! oc = self.dup - oc.name = "COPY OF #{oc.name}" + oc.name = I18n.t("models.order_cycle.cloned_order_cycle_name", order_cycle: oc.name) oc.orders_open_at = oc.orders_close_at = nil oc.coordinator_fee_ids = self.coordinator_fee_ids oc.preferred_product_selection_from_coordinator_inventory_only = self.preferred_product_selection_from_coordinator_inventory_only diff --git a/config/locales/en.yml b/config/locales/en.yml index f34706ca29..3654cffcf9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -88,6 +88,11 @@ en: user_passwords: spree_user: updated_not_active: "Your password has been reset, but your email has not been confirmed yet." + + models: + order_cycle: + cloned_order_cycle_name: "COPY OF %{order_cycle}" + enterprise_mailer: confirmation_instructions: subject: "Please confirm the email address for %{enterprise}" From 003e65fbeda8566b4721dd29f246d0397ce1364d Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sat, 8 Sep 2018 00:33:49 +0800 Subject: [PATCH 052/190] Translate error when failing to create order cycle --- .../admin/order_cycles/services/order_cycle.js.coffee | 4 ++-- config/locales/en.yml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index 59d9b4f994..cac1c9ca87 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -156,7 +156,7 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, S if response.data.errors? StatusMessage.display('failure', response.data.errors[0]) else - StatusMessage.display('failure', 'Failed to create order cycle') + StatusMessage.display('failure', t('js.order_cycles.create_failure')) update: (destination, form) -> return unless @confirmNoDistributors() @@ -171,7 +171,7 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, S if response.data.errors? StatusMessage.display('failure', response.data.errors[0]) else - StatusMessage.display('failure', 'Failed to create order cycle') + StatusMessage.display('failure', t('js.order_cycles.update_failure')) confirmNoDistributors: -> diff --git a/config/locales/en.yml b/config/locales/en.yml index f34706ca29..5b54f0530a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2565,7 +2565,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using This will set stock level to zero on all products for this enterprise that are not present in the uploaded file. order_cycles: + create_failure: "Failed to create order cycle" update_success: 'Your order cycle has been updated.' + update_failure: "Failed to update order cycle" no_distributors: There are no distributors in this order cycle. This order cycle will not be visible to customers until you add one. Would you like to continue saving this order cycle?' enterprises: producer: "Producer" From 02a909c1b1f500277fa9c3c5366fdc8c34ddb8f2 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sat, 8 Sep 2018 23:10:06 +0800 Subject: [PATCH 053/190] Do not use image_tag for enterprise image previews If Rails config.assets.compile is false and config.assets.digest is true, which is the case for staging and production, image_tag fails when the image file is not found. We do not need sprockets to ensure presence of the image file for these tags, because the correct SRC values are assigned through the JS. Inserting HTML should be sufficient. --- app/views/admin/enterprises/form/_images.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml index d44e619cf6..0f48fce322 100644 --- a/app/views/admin/enterprises/form/_images.html.haml +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -4,7 +4,7 @@ %br 100 x 100 pixels .omega.eight.columns - = image_tag '', class: 'image-field-group__preview-image', 'ng-src' => '{{ Enterprise.logo.medium }}', 'ng-if' => 'Enterprise.logo' + %img{ class: 'image-field-group__preview-image', ng: { src: '{{ Enterprise.logo.medium }}', if: 'Enterprise.logo' } } = f.file_field :logo %a.button.red{ href: '', ng: {click: 'removeLogo()', if: 'Enterprise.logo'} } = t('admin.enterprises.remove_logo.remove') @@ -20,7 +20,7 @@ = t('.promo_image_note3') .omega.eight.columns - = image_tag '', class: 'image-field-group__preview-image', 'ng-src' => '{{ Enterprise.promo_image.large }}', 'ng-if' => 'Enterprise.promo_image' + %img{ class: 'image-field-group__preview-image', ng: { src: '{{ Enterprise.promo_image.large }}', if: 'Enterprise.promo_image' } } = f.file_field :promo_image %a.button.red{ href: '', ng: {click: 'removePromoImage()', if: 'Enterprise.promo_image'} } = t('admin.enterprises.remove_promo_image.remove') From 980c46c63fdb1232e33f17ca73e8ab5df7ad253c Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:22:54 +0100 Subject: [PATCH 054/190] Convert Spree view to HAML --- app/views/spree/admin/orders/index.html.haml | 91 ++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/views/spree/admin/orders/index.html.haml diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml new file mode 100644 index 0000000000..0eb540f349 --- /dev/null +++ b/app/views/spree/admin/orders/index.html.haml @@ -0,0 +1,91 @@ +- content_for :page_title do + = t(:listing_orders) +- content_for :page_actions do + %li + = button_link_to t(:new_order), new_admin_order_url, :icon => 'icon-plus', :id => 'admin_new_order' +- content_for :table_filter_title do + = t(:search) +- content_for :table_filter do + %div{"data-hook" => "admin_orders_index_search"} + = search_form_for [:admin, @search] do |f| + .field-block.alpha.four.columns + .date-range-filter.field + = label_tag nil, t(:date_range) + .date-range-fields + = f.text_field :created_at_gt, :class => 'datepicker datepicker-from', :value => params[:q][:created_at_gt], :placeholder => t(:start) + %span.range-divider + %i.icon-arrow-right + = f.text_field :created_at_lt, :class => 'datepicker datepicker-to', :value => params[:q][:created_at_lt], :placeholder => t(:stop) + .field + = label_tag nil, t(:status) + = f.select :state_eq, Spree::Order.state_machines[:state].states.collect {|s| [t("order_state.#{s.name}"), s.value]}, {:include_blank => true}, :class => 'select2' + .four.columns + .field + = label_tag nil, t(:order_number) + = f.text_field :number_cont + .field + = label_tag nil, t(:email) + = f.email_field :email_cont + .four.columns + .field + = label_tag nil, t(:first_name_begins_with) + = f.text_field :bill_address_firstname_start, :size => 25 + .field + = label_tag nil, t(:last_name_begins_with) + = f.text_field :bill_address_lastname_start, :size => 25 + .omega.four.columns + .field.checkbox + %label + = f.check_box :completed_at_not_null, {:checked => @show_only_completed}, '1', '' + = t(:show_only_complete_orders) + .field.checkbox + %label + = f.check_box :inventory_units_shipment_id_null, { }, '1', '0' + = t(:show_only_unfulfilled_orders) + .clearfix + .actions.filter-actions + %div{"data-hook" => "admin_orders_index_search_buttons"} + = button t(:filter_results), 'icon-search' +- unless @orders.empty? + %table#listing_orders.index.responsive{"data-hook" => "", width: "100%"} + %colgroup + %col{style: "width: 16%;"} + %col{style: "width: 10%;"} + %col{style: "width: 13%;"} + %col{style: "width: 13%;"} + %col{style: "width: 13%;"} + %col{style: "width: 17%;"} + %col{style: "width: 10%;"} + %col{style: "width: 8%;"} + %thead + %tr{"data-hook" => "admin_orders_index_headers"} + - if @show_only_completed + %th= sort_link @search, :completed_at, t(:completed_at, :scope => 'activerecord.attributes.spree/order') + - else + %th= sort_link @search, :created_at, t(:created_at, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :number, t(:number, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :state, t(:state, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :payment_state, t(:payment_state, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :shipment_state, t(:shipment_state, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :email, t(:email, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :total, t(:total, :scope => 'activerecord.attributes.spree/order') + %th.actions{"data-hook" => "admin_orders_index_header_actions"} + %tbody + - @orders.each do |order| + %tr{class: "state-#{order.state.downcase} #{cycle('odd', 'even')}", "data-hook" => "admin_orders_index_rows"} + %td.align-center= l (@show_only_completed ? order.completed_at : order.created_at).to_date + %td= link_to order.number, admin_order_path(order) + %td.align-center + %span{class: "state #{order.state.downcase}"}= t("order_state.#{order.state.downcase}") + %td.align-center + %span{class: "state #{order.payment_state}"}= link_to t("payment_states.#{order.payment_state}"), admin_order_payments_path(order) if order.payment_state + %td.align-center + %span{class: "state #{order.shipment_state}"}= link_to t("shipment_states.#{order.shipment_state}"), admin_order_shipments_path(order) if order.shipment_state + %td= mail_to order.email + %td.align-center= order.display_total.to_html + %td.actions.align-center{"data-hook" => "admin_orders_index_row_actions"} + = link_to_edit_url edit_admin_order_path(order), :title => "admin_edit_#{dom_id(order)}", :no_text => true +- else + .no-objects-found + = t(:no_orders_found) += paginate @orders \ No newline at end of file From 5a2491cb47d9b8b96f8c901343618b9402b87616 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:30:14 +0100 Subject: [PATCH 055/190] Apply ng-app override --- app/overrides/spree/admin/orders/index/set_ng_app.deface | 2 -- app/views/spree/admin/orders/index.html.haml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 app/overrides/spree/admin/orders/index/set_ng_app.deface diff --git a/app/overrides/spree/admin/orders/index/set_ng_app.deface b/app/overrides/spree/admin/orders/index/set_ng_app.deface deleted file mode 100644 index 9ca071be11..0000000000 --- a/app/overrides/spree/admin/orders/index/set_ng_app.deface +++ /dev/null @@ -1,2 +0,0 @@ -add_to_attributes "table#listing_orders" -attributes "ng-app" => "ofn.admin" diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 0eb540f349..bd38bfbf5a 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -47,7 +47,7 @@ %div{"data-hook" => "admin_orders_index_search_buttons"} = button t(:filter_results), 'icon-search' - unless @orders.empty? - %table#listing_orders.index.responsive{"data-hook" => "", width: "100%"} + %table#listing_orders.index.responsive{"data-hook" => "", width: "100%", "ng-app" => "ofn.admin"} %colgroup %col{style: "width: 16%;"} %col{style: "width: 10%;"} From c7e2f817ed0e9b2a873a186e27a3a9bb1a4c8cec Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:34:34 +0100 Subject: [PATCH 056/190] Apply colgroup override --- .../admin/orders/index/rearrange_cols.html.haml.deface | 5 ----- app/views/spree/admin/orders/index.html.haml | 9 +-------- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 app/overrides/spree/admin/orders/index/rearrange_cols.html.haml.deface diff --git a/app/overrides/spree/admin/orders/index/rearrange_cols.html.haml.deface b/app/overrides/spree/admin/orders/index/rearrange_cols.html.haml.deface deleted file mode 100644 index 39344b460e..0000000000 --- a/app/overrides/spree/admin/orders/index/rearrange_cols.html.haml.deface +++ /dev/null @@ -1,5 +0,0 @@ -/ replace_contents "table#listing_orders colgroup" --# See also: add_capture_order_shortcut, admin/orders/index/add_distributor_*to_admin_orders - -%col{style: "width: 10%"} --# There are 8 other columns, but they seem to sort themselves out :) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index bd38bfbf5a..1cc9bfc8a8 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -49,14 +49,7 @@ - unless @orders.empty? %table#listing_orders.index.responsive{"data-hook" => "", width: "100%", "ng-app" => "ofn.admin"} %colgroup - %col{style: "width: 16%;"} - %col{style: "width: 10%;"} - %col{style: "width: 13%;"} - %col{style: "width: 13%;"} - %col{style: "width: 13%;"} - %col{style: "width: 17%;"} - %col{style: "width: 10%;"} - %col{style: "width: 8%;"} + %col{style: "width: 10%"} %thead %tr{"data-hook" => "admin_orders_index_headers"} - if @show_only_completed From 1958ac9932140d669370907cb589ea7bd5724c72 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:39:05 +0100 Subject: [PATCH 057/190] Apply distributor filter inputs override --- ...r_and_order_cycle_filter_inputs.html.haml.deface | 13 ------------- app/views/spree/admin/orders/index.html.haml | 11 +++++++++++ 2 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 app/overrides/spree/admin/orders/index/add_distributor_and_order_cycle_filter_inputs.html.haml.deface diff --git a/app/overrides/spree/admin/orders/index/add_distributor_and_order_cycle_filter_inputs.html.haml.deface b/app/overrides/spree/admin/orders/index/add_distributor_and_order_cycle_filter_inputs.html.haml.deface deleted file mode 100644 index 7068d94d2d..0000000000 --- a/app/overrides/spree/admin/orders/index/add_distributor_and_order_cycle_filter_inputs.html.haml.deface +++ /dev/null @@ -1,13 +0,0 @@ -/ insert_before "div.clearfix" - -.field-block.alpha.eight.columns - = label_tag nil, t(:distributors) - = select_tag("q[distributor_id_in]", - options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]), - {class: "select2 fullwidth", multiple: true}) - -.field-block.alpha.eight.columns - = label_tag nil, t(:order_cycles) - = select_tag("q[order_cycle_id_in]", - options_for_select(OrderCycle.managed_by(spree_current_user).where('order_cycles.orders_close_at is not null').order('order_cycles.orders_close_at DESC').map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]), - {class: "select2 fullwidth", multiple: true}) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 1cc9bfc8a8..cfe3b3c2d9 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -42,6 +42,17 @@ %label = f.check_box :inventory_units_shipment_id_null, { }, '1', '0' = t(:show_only_unfulfilled_orders) + .field-block.alpha.eight.columns + = label_tag nil, t(:distributors) + = select_tag("q[distributor_id_in]", + options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]), + {class: "select2 fullwidth", multiple: true}) + + .field-block.alpha.eight.columns + = label_tag nil, t(:order_cycles) + = select_tag("q[order_cycle_id_in]", + options_for_select(OrderCycle.managed_by(spree_current_user).where('order_cycles.orders_close_at is not null').order('order_cycles.orders_close_at DESC').map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]), + {class: "select2 fullwidth", multiple: true}) .clearfix .actions.filter-actions %div{"data-hook" => "admin_orders_index_search_buttons"} From 6389c552ca80daefa703a98140ed5a11341ae0a0 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:41:25 +0100 Subject: [PATCH 058/190] Apply distributor th override --- .../admin/orders/index/add_distributor_th.html.haml.deface | 4 ---- app/views/spree/admin/orders/index.html.haml | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 app/overrides/spree/admin/orders/index/add_distributor_th.html.haml.deface diff --git a/app/overrides/spree/admin/orders/index/add_distributor_th.html.haml.deface b/app/overrides/spree/admin/orders/index/add_distributor_th.html.haml.deface deleted file mode 100644 index bba71ef434..0000000000 --- a/app/overrides/spree/admin/orders/index/add_distributor_th.html.haml.deface +++ /dev/null @@ -1,4 +0,0 @@ -/ insert_top "[data-hook='admin_orders_index_headers']" - -%th - = t(:products_distributor) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index cfe3b3c2d9..54ff656939 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -63,6 +63,8 @@ %col{style: "width: 10%"} %thead %tr{"data-hook" => "admin_orders_index_headers"} + %th + = t(:products_distributor) - if @show_only_completed %th= sort_link @search, :completed_at, t(:completed_at, :scope => 'activerecord.attributes.spree/order') - else From ba0701457e4a40407fab11bcfa4ade04ccf8d6d8 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:42:08 +0100 Subject: [PATCH 059/190] Apply distributor td override --- .../admin/orders/index/add_distributor_td.html.haml.deface | 4 ---- app/views/spree/admin/orders/index.html.haml | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface diff --git a/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface b/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface deleted file mode 100644 index 5d9b7ab083..0000000000 --- a/app/overrides/spree/admin/orders/index/add_distributor_td.html.haml.deface +++ /dev/null @@ -1,4 +0,0 @@ -/ insert_top "[data-hook='admin_orders_index_rows']" - -%td.align-center - = order.distributor.andand.name diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 54ff656939..1bb334e4ad 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -79,6 +79,8 @@ %tbody - @orders.each do |order| %tr{class: "state-#{order.state.downcase} #{cycle('odd', 'even')}", "data-hook" => "admin_orders_index_rows"} + %td.align-center + = order.distributor.andand.name %td.align-center= l (@show_only_completed ? order.completed_at : order.created_at).to_date %td= link_to order.number, admin_order_path(order) %td.align-center From 8f174b130861432753e75d0e9edf2b7dd21b3d77 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:43:38 +0100 Subject: [PATCH 060/190] Apply ship shortcut override --- .../admin/orders/index/add_ship_shortcut.html.haml.deface | 6 ------ app/views/spree/admin/orders/index.html.haml | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface diff --git a/app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface b/app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface deleted file mode 100644 index a0ac8a7266..0000000000 --- a/app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface +++ /dev/null @@ -1,6 +0,0 @@ -/ insert_bottom "[data-hook='admin_orders_index_row_actions']" --# See also: app/overrides/add_capture_order_shortcut.rb - -- if order.ready_to_ship? - - # copied from backend/app/views/spree/admin/payments/_list.html.erb - = link_to_with_icon "icon-road", t('admin.orders.index.ship'), fire_admin_order_url(order, :e => 'ship'), :method => :put, :no_text => true, :data => {:action => 'ship', :confirm => t(:are_you_sure)} diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 1bb334e4ad..adfe35ee38 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -93,6 +93,9 @@ %td.align-center= order.display_total.to_html %td.actions.align-center{"data-hook" => "admin_orders_index_row_actions"} = link_to_edit_url edit_admin_order_path(order), :title => "admin_edit_#{dom_id(order)}", :no_text => true + - if order.ready_to_ship? + - # copied from backend/app/views/spree/admin/payments/_list.html.erb + = link_to_with_icon "icon-road", t('admin.orders.index.ship'), fire_admin_order_url(order, :e => 'ship'), :method => :put, :no_text => true, :data => {:action => 'ship', :confirm => t(:are_you_sure)} - else .no-objects-found = t(:no_orders_found) From afedaea2b52aea121754ac4d263a3c737b1d3811 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:45:40 +0100 Subject: [PATCH 061/190] Apply special instructions override --- .../orders/index/add_special_instructions.html.haml.deface | 6 ------ app/views/spree/admin/orders/index.html.haml | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface diff --git a/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface b/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface deleted file mode 100644 index 1711343f3c..0000000000 --- a/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface +++ /dev/null @@ -1,6 +0,0 @@ -/ insert_bottom "[data-hook='admin_orders_index_rows'] td:nth-child(3)" - -- if order.special_instructions.present? - %br - %span{class: "icon-warning-sign", "ofn-with-tip" => order.special_instructions} - notes diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index adfe35ee38..8c76fe630a 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -82,7 +82,12 @@ %td.align-center = order.distributor.andand.name %td.align-center= l (@show_only_completed ? order.completed_at : order.created_at).to_date - %td= link_to order.number, admin_order_path(order) + %td + = link_to order.number, admin_order_path(order) + - if order.special_instructions.present? + %br + %span{class: "icon-warning-sign", "ofn-with-tip" => order.special_instructions} + notes %td.align-center %span{class: "state #{order.state.downcase}"}= t("order_state.#{order.state.downcase}") %td.align-center From 507f4d08789167b40c3cde21b08c1175974092d9 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:53:37 +0100 Subject: [PATCH 062/190] OCD tidy up of visually misaligned filter --- app/views/spree/admin/orders/index.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 8c76fe630a..507fedc8fc 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -47,8 +47,7 @@ = select_tag("q[distributor_id_in]", options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]), {class: "select2 fullwidth", multiple: true}) - - .field-block.alpha.eight.columns + .field-block.omega.eight.columns = label_tag nil, t(:order_cycles) = select_tag("q[order_cycle_id_in]", options_for_select(OrderCycle.managed_by(spree_current_user).where('order_cycles.orders_close_at is not null').order('order_cycles.orders_close_at DESC').map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]), From f08d6389d8b1801e14f05c3ab5190fbccdd36d37 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 12:06:58 +0100 Subject: [PATCH 063/190] Apply capture order override --- app/assets/stylesheets/admin/orders.css.scss | 4 ++++ app/overrides/add_capture_order_shortcut.rb | 11 ----------- app/views/spree/admin/orders/index.html.haml | 3 ++- 3 files changed, 6 insertions(+), 12 deletions(-) delete mode 100644 app/overrides/add_capture_order_shortcut.rb diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 46e6b8aa0f..33c5a4a3e8 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -87,3 +87,7 @@ div#group_buy_calculation { th.actions { white-space: nowrap; } + +table.index td.actions { + text-align: left; +} diff --git a/app/overrides/add_capture_order_shortcut.rb b/app/overrides/add_capture_order_shortcut.rb deleted file mode 100644 index db6947cf08..0000000000 --- a/app/overrides/add_capture_order_shortcut.rb +++ /dev/null @@ -1,11 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/admin/orders/index", - :name => "add_capture_order_shortcut", - :insert_bottom => "[data-hook='admin_orders_index_row_actions']", - :partial => 'spree/admin/orders/capture' - ) -# And align actions column (not spree standard, but looks better IMO) -Deface::Override.new(:virtual_path => "spree/admin/orders/index", - :name => "add_capture_order_shortcut_align", - :set_attributes => "[data-hook='admin_orders_index_row_actions']", - :attributes => {:class => "actions", :style => "text-align:left;"} #removes 'align-center' class - ) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 507fedc8fc..dae0c47089 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -95,11 +95,12 @@ %span{class: "state #{order.shipment_state}"}= link_to t("shipment_states.#{order.shipment_state}"), admin_order_shipments_path(order) if order.shipment_state %td= mail_to order.email %td.align-center= order.display_total.to_html - %td.actions.align-center{"data-hook" => "admin_orders_index_row_actions"} + %td.actions{"data-hook" => "admin_orders_index_row_actions"} = link_to_edit_url edit_admin_order_path(order), :title => "admin_edit_#{dom_id(order)}", :no_text => true - if order.ready_to_ship? - # copied from backend/app/views/spree/admin/payments/_list.html.erb = link_to_with_icon "icon-road", t('admin.orders.index.ship'), fire_admin_order_url(order, :e => 'ship'), :method => :put, :no_text => true, :data => {:action => 'ship', :confirm => t(:are_you_sure)} + = render partial: 'spree/admin/orders/capture', locals: {order: order} - else .no-objects-found = t(:no_orders_found) From 4d9ebf0550ae2e1eabee4e62f43eca9ec7cfb3fc Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 11 Sep 2018 12:17:28 +0100 Subject: [PATCH 064/190] Apply submenu override --- app/overrides/add_orders_admin_sub_menu.rb | 4 ---- app/views/spree/admin/orders/index.html.haml | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 app/overrides/add_orders_admin_sub_menu.rb diff --git a/app/overrides/add_orders_admin_sub_menu.rb b/app/overrides/add_orders_admin_sub_menu.rb deleted file mode 100644 index ff26145421..0000000000 --- a/app/overrides/add_orders_admin_sub_menu.rb +++ /dev/null @@ -1,4 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/admin/orders/index", - :name => "add_orders_admin_sub_menu", - :insert_before => "code[erb-silent]:contains('content_for :table_filter_title do')", - :text => "<%= render :partial => 'spree/admin/shared/order_sub_menu' %>") diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index dae0c47089..3b233424cc 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -3,6 +3,7 @@ - content_for :page_actions do %li = button_link_to t(:new_order), new_admin_order_url, :icon => 'icon-plus', :id => 'admin_new_order' += render partial: 'spree/admin/shared/order_sub_menu' - content_for :table_filter_title do = t(:search) - content_for :table_filter do @@ -56,6 +57,7 @@ .actions.filter-actions %div{"data-hook" => "admin_orders_index_search_buttons"} = button t(:filter_results), 'icon-search' + - unless @orders.empty? %table#listing_orders.index.responsive{"data-hook" => "", width: "100%", "ng-app" => "ofn.admin"} %colgroup @@ -104,4 +106,5 @@ - else .no-objects-found = t(:no_orders_found) -= paginate @orders \ No newline at end of file + += paginate @orders From 91c0de3f2af456b481cce03d2921b946497b0719 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Wed, 12 Sep 2018 11:09:57 +0000 Subject: [PATCH 065/190] Bump db2fog from 0.8.0 to 0.9.0 Bumps [db2fog](https://github.com/itbeaver/db2fog) from 0.8.0 to 0.9.0. - [Release notes](https://github.com/itbeaver/db2fog/releases) - [Changelog](https://github.com/itbeaver/db2fog/blob/master/CHANGELOG.md) - [Commits](https://github.com/itbeaver/db2fog/commits/v0.9.0) Signed-off-by: dependabot[bot] --- Gemfile.lock | 122 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 38 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 70f0963514..9349fa46d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -136,7 +136,7 @@ GIT GEM remote: https://rubygems.org/ specs: - CFPropertyList (2.3.2) + CFPropertyList (2.3.6) actionmailer (3.2.22.5) actionpack (= 3.2.22.5) mail (~> 2.5.4) @@ -246,10 +246,10 @@ GEM daemons (1.2.2) dalli (2.7.2) database_cleaner (0.7.1) - db2fog (0.8.0) - activerecord (~> 3.0) + db2fog (0.9.0) + activerecord (>= 3.2.0, < 5.0) fog (~> 1.0) - rails (~> 3.0) + rails (>= 3.2.0, < 5.0) debugger-linecache (1.2.0) delayed_job (4.1.4) activesupport (>= 3.0, < 5.2) @@ -265,12 +265,13 @@ GEM devise (>= 2.1.0) diff-lcs (1.3) diffy (3.1.0) + dry-inflector (0.1.2) em-websocket (0.5.1) eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) erubis (2.7.0) eventmachine (1.2.7) - excon (0.45.4) + excon (0.62.0) execjs (2.6.0) factory_bot (4.8.2) activesupport (>= 3.0.0) @@ -286,19 +287,26 @@ GEM rails (>= 3, < 5) fission (0.5.0) CFPropertyList (~> 2.2) - fog (1.36.0) + fog (1.41.0) fog-aliyun (>= 0.1.0) fog-atmos fog-aws (>= 0.6.0) fog-brightbox (~> 0.4) - fog-core (~> 1.32) + fog-cloudatcost (~> 0.1.0) + fog-core (~> 1.45) + fog-digitalocean (>= 0.3.0) + fog-dnsimple (~> 1.0) fog-dynect (~> 0.0.2) fog-ecloud (~> 0.1) fog-google (<= 0.1.0) + fog-internet-archive + fog-joyent fog-json fog-local + fog-openstack fog-powerdns (>= 0.1.1) fog-profitbricks + fog-rackspace fog-radosgw (>= 0.0.2) fog-riakcs fog-sakuracloud (>= 0.0.4) @@ -308,32 +316,47 @@ GEM fog-terremark fog-vmfusion fog-voxel + fog-vsphere (>= 0.4.0) fog-xenserver fog-xml (~> 0.1.1) ipaddress (~> 0.5) - nokogiri (~> 1.5, >= 1.5.11) - fog-aliyun (0.1.0) - fog-core (~> 1.27) - fog-json (~> 1.0) + json (>= 1.8, < 2.0) + fog-aliyun (0.3.2) + fog-core + fog-json ipaddress (~> 0.8) xml-simple (~> 1.1) fog-atmos (0.1.0) fog-core fog-xml - fog-aws (0.7.6) - fog-core (~> 1.27) + fog-aws (2.0.1) + fog-core (~> 1.38) fog-json (~> 1.0) fog-xml (~> 0.1) ipaddress (~> 0.8) - fog-brightbox (0.9.0) - fog-core (~> 1.22) + fog-brightbox (0.16.1) + dry-inflector + fog-core fog-json - inflecto (~> 0.0.2) - fog-core (1.35.0) + mime-types + fog-cloudatcost (0.1.2) + fog-core (~> 1.36) + fog-json (~> 1.0) + fog-xml (~> 0.1) + ipaddress (~> 0.8) + fog-core (1.45.0) builder - excon (~> 0.45) + excon (~> 0.58) formatador (~> 0.2) - fog-dynect (0.0.2) + fog-digitalocean (0.4.0) + fog-core + fog-json + fog-xml + ipaddress (>= 0.5) + fog-dnsimple (1.0.0) + fog-core (~> 1.38) + fog-json (~> 1.0) + fog-dynect (0.0.3) fog-core fog-json fog-xml @@ -344,20 +367,35 @@ GEM fog-core fog-json fog-xml - fog-json (1.0.2) - fog-core (~> 1.0) - multi_json (~> 1.10) - fog-local (0.2.1) - fog-core (~> 1.27) - fog-powerdns (0.1.1) - fog-core (~> 1.27) - fog-json (~> 1.0) - fog-xml (~> 0.1) - fog-profitbricks (0.0.5) + fog-internet-archive (0.0.1) fog-core + fog-json fog-xml - nokogiri - fog-radosgw (0.0.4) + fog-joyent (0.0.1) + fog-core (~> 1.42) + fog-json (>= 1.0) + fog-json (1.2.0) + fog-core + multi_json (~> 1.10) + fog-local (0.6.0) + fog-core (>= 1.27, < 3.0) + fog-openstack (0.1.25) + fog-core (~> 1.40) + fog-json (>= 1.0) + ipaddress (>= 0.8) + fog-powerdns (0.2.0) + fog-core + fog-json + fog-xml + fog-profitbricks (4.1.1) + fog-core (~> 1.42) + fog-json (~> 1.0) + fog-rackspace (0.1.6) + fog-core (>= 1.35) + fog-json (>= 1.0) + fog-xml (>= 0.1) + ipaddress (>= 0.8) + fog-radosgw (0.0.5) fog-core (>= 1.21.0) fog-json fog-xml (>= 0.0.1) @@ -365,13 +403,13 @@ GEM fog-core fog-json fog-xml - fog-sakuracloud (1.4.0) + fog-sakuracloud (1.7.5) fog-core fog-json fog-serverlove (0.1.2) fog-core fog-json - fog-softlayer (1.0.2) + fog-softlayer (1.1.4) fog-core fog-json fog-storm_on_demand (0.1.1) @@ -386,12 +424,15 @@ GEM fog-voxel (0.1.0) fog-core fog-xml - fog-xenserver (0.2.2) + fog-vsphere (2.3.0) + fog-core + rbvmomi (~> 1.9) + fog-xenserver (0.3.0) fog-core fog-xml - fog-xml (0.1.2) + fog-xml (0.1.3) fog-core - nokogiri (~> 1.5, >= 1.5.11) + nokogiri (>= 1.5.11, < 2.0.0) foreigner (1.7.4) activerecord (>= 3.0.0) formatador (0.2.5) @@ -442,8 +483,7 @@ GEM i18n (>= 0.6.6, < 2) immigrant (0.3.6) activerecord (>= 3.0) - inflecto (0.0.2) - ipaddress (0.8.0) + ipaddress (0.8.3) journey (1.0.4) jquery-migrate-rails (1.2.1) jquery-rails (2.2.2) @@ -571,6 +611,11 @@ GEM rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) + rbvmomi (1.13.0) + builder (~> 3.0) + json (>= 1.8) + nokogiri (~> 1.5) + trollop (~> 2.1) rdoc (3.12.2) json (~> 1.4) redcarpet (3.2.3) @@ -664,6 +709,7 @@ GEM treetop (1.4.15) polyglot polyglot (>= 0.3.1) + trollop (2.9.9) truncate_html (0.9.2) turbo-sprockets-rails3 (0.3.6) railties (> 3.2.8, < 4.0.0) From cc43d789e20fb8ca03e1fb3bb0a59570b2b35b67 Mon Sep 17 00:00:00 2001 From: Hugo Daniel Date: Wed, 12 Sep 2018 14:51:53 +0200 Subject: [PATCH 066/190] Refacto edit method for content controller and enable custom user guide link --- app/controllers/admin/contents_controller.rb | 21 ++++++++------- app/models/content_configuration.rb | 3 +++ .../footer_and_external_links_section.rb | 23 ++++++++++++++++ .../group_signup_page_section.rb | 15 +++++++++++ .../preference_sections/header_section.rb | 15 +++++++++++ .../preference_sections/home_page_section.rb | 14 ++++++++++ .../hub_signup_page_section.rb | 15 +++++++++++ .../preference_sections/main_links_section.rb | 26 +++++++++++++++++++ .../producer_signup_page_section.rb | 15 +++++++++++ .../preference_sections/user_guide_section.rb | 11 ++++++++ .../admin/shared/_user_guide_link.html.haml | 2 +- app/views/enterprise_mailer/welcome.html.haml | 2 +- config/locales/en.yml | 3 +++ spec/features/admin/content_spec.rb | 24 ++++++++++++----- 14 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 app/models/preference_sections/footer_and_external_links_section.rb create mode 100644 app/models/preference_sections/group_signup_page_section.rb create mode 100644 app/models/preference_sections/header_section.rb create mode 100644 app/models/preference_sections/home_page_section.rb create mode 100644 app/models/preference_sections/hub_signup_page_section.rb create mode 100644 app/models/preference_sections/main_links_section.rb create mode 100644 app/models/preference_sections/producer_signup_page_section.rb create mode 100644 app/models/preference_sections/user_guide_section.rb diff --git a/app/controllers/admin/contents_controller.rb b/app/controllers/admin/contents_controller.rb index 9eac6f279a..28f117c52c 100644 --- a/app/controllers/admin/contents_controller.rb +++ b/app/controllers/admin/contents_controller.rb @@ -1,15 +1,9 @@ module Admin class ContentsController < Spree::Admin::BaseController def edit - @preference_sections = [{name: I18n.t('admin.contents.edit.header'), preferences: [:logo, :logo_mobile, :logo_mobile_svg]}, - {name: I18n.t('admin.contents.edit.home_page'), preferences: [:home_hero, :home_show_stats]}, - {name: I18n.t('admin.contents.edit.producer_signup_page'), preferences: [:producer_signup_pricing_table_html, :producer_signup_case_studies_html, :producer_signup_detail_html]}, - {name: I18n.t('admin.contents.edit.hub_signup_page'), preferences: [:hub_signup_pricing_table_html, :hub_signup_case_studies_html, :hub_signup_detail_html]}, - {name: I18n.t('admin.contents.edit.group_signup_page'), preferences: [:group_signup_pricing_table_html, :group_signup_case_studies_html, :group_signup_detail_html]}, - {name: I18n.t('admin.contents.edit.main_links'), preferences: [:menu_1, :menu_1_icon_name, :menu_2, :menu_2_icon_name, :menu_3, :menu_3_icon_name, :menu_4, :menu_4_icon_name, :menu_5, :menu_5_icon_name, :menu_6, :menu_6_icon_name, :menu_7, :menu_7_icon_name]}, - {name: I18n.t('admin.contents.edit.footer_and_external_links'), preferences: [:footer_logo, - :footer_facebook_url, :footer_twitter_url, :footer_instagram_url, :footer_linkedin_url, :footer_googleplus_url, :footer_pinterest_url, - :footer_email, :community_forum_url, :footer_links_md, :footer_about_url]}] + @preference_sections = preference_sections.map do |preference_section| + { name: preference_section.name, preferences: preference_section.preferences } + end end def update @@ -26,5 +20,14 @@ module Admin redirect_to main_app.edit_admin_content_path end + + private + + def preference_sections + Dir["app/models/preference_sections/*.rb"].map do |filename| + basename = 'PreferenceSections::' + File.basename(filename, '.rb').camelize + basename.constantize.new + end + end end end diff --git a/app/models/content_configuration.rb b/app/models/content_configuration.rb index 19e1d07f9e..1780389870 100644 --- a/app/models/content_configuration.rb +++ b/app/models/content_configuration.rb @@ -69,4 +69,7 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration EOS preference :footer_about_url, :string, default: "http://www.openfoodnetwork.org/ofn-local/open-food-network-australia/" + + #User Guide + preference :user_guide_link, :string, default: 'http://www.openfoodnetwork.org/platform/user-guide/' end diff --git a/app/models/preference_sections/footer_and_external_links_section.rb b/app/models/preference_sections/footer_and_external_links_section.rb new file mode 100644 index 0000000000..cf9d926553 --- /dev/null +++ b/app/models/preference_sections/footer_and_external_links_section.rb @@ -0,0 +1,23 @@ +module PreferenceSections + class FooterAndExternalLinksSection + def name + I18n.t('admin.contents.edit.footer_and_external_links') + end + + def preferences + [ + :footer_logo, + :footer_facebook_url, + :footer_twitter_url, + :footer_instagram_url, + :footer_linkedin_url, + :footer_googleplus_url, + :footer_pinterest_url, + :footer_email, + :community_forum_url, + :footer_links_md, + :footer_about_url + ] + end + end +end diff --git a/app/models/preference_sections/group_signup_page_section.rb b/app/models/preference_sections/group_signup_page_section.rb new file mode 100644 index 0000000000..b28570fcd6 --- /dev/null +++ b/app/models/preference_sections/group_signup_page_section.rb @@ -0,0 +1,15 @@ +module PreferenceSections + class GroupSignupPageSection + def name + I18n.t('admin.contents.edit.group_signup_page') + end + + def preferences + [ + :group_signup_pricing_table_html, + :group_signup_case_studies_html, + :group_signup_detail_html + ] + end + end +end diff --git a/app/models/preference_sections/header_section.rb b/app/models/preference_sections/header_section.rb new file mode 100644 index 0000000000..87e09d6e03 --- /dev/null +++ b/app/models/preference_sections/header_section.rb @@ -0,0 +1,15 @@ +module PreferenceSections + class HeaderSection + def name + I18n.t('admin.contents.edit.header') + end + + def preferences + [ + :logo, + :logo_mobile, + :logo_mobile_svg + ] + end + end +end diff --git a/app/models/preference_sections/home_page_section.rb b/app/models/preference_sections/home_page_section.rb new file mode 100644 index 0000000000..05e58c5523 --- /dev/null +++ b/app/models/preference_sections/home_page_section.rb @@ -0,0 +1,14 @@ +module PreferenceSections + class HomePageSection + def name + I18n.t('admin.contents.edit.home_page') + end + + def preferences + [ + :home_hero, + :home_show_stats + ] + end + end +end diff --git a/app/models/preference_sections/hub_signup_page_section.rb b/app/models/preference_sections/hub_signup_page_section.rb new file mode 100644 index 0000000000..3c80e3ebff --- /dev/null +++ b/app/models/preference_sections/hub_signup_page_section.rb @@ -0,0 +1,15 @@ +module PreferenceSections + class HubSignupPageSection + def name + I18n.t('admin.contents.edit.hub_signup_page') + end + + def preferences + [ + :hub_signup_pricing_table_html, + :hub_signup_case_studies_html, + :hub_signup_detail_html + ] + end + end +end diff --git a/app/models/preference_sections/main_links_section.rb b/app/models/preference_sections/main_links_section.rb new file mode 100644 index 0000000000..b23833650f --- /dev/null +++ b/app/models/preference_sections/main_links_section.rb @@ -0,0 +1,26 @@ +module PreferenceSections + class MainLinksSection + def name + I18n.t('admin.contents.edit.main_links') + end + + def preferences + [ + :menu_1, + :menu_1_icon_name, + :menu_2, + :menu_2_icon_name, + :menu_3, + :menu_3_icon_name, + :menu_4, + :menu_4_icon_name, + :menu_5, + :menu_5_icon_name, + :menu_6, + :menu_6_icon_name, + :menu_7, + :menu_7_icon_name + ] + end + end +end diff --git a/app/models/preference_sections/producer_signup_page_section.rb b/app/models/preference_sections/producer_signup_page_section.rb new file mode 100644 index 0000000000..bf96894c53 --- /dev/null +++ b/app/models/preference_sections/producer_signup_page_section.rb @@ -0,0 +1,15 @@ +module PreferenceSections + class ProducerSignupPageSection + def name + I18n.t('admin.contents.edit.producer_signup_page') + end + + def preferences + [ + :producer_signup_pricing_table_html, + :producer_signup_case_studies_html, + :producer_signup_detail_html + ] + end + end +end diff --git a/app/models/preference_sections/user_guide_section.rb b/app/models/preference_sections/user_guide_section.rb new file mode 100644 index 0000000000..4c1323959f --- /dev/null +++ b/app/models/preference_sections/user_guide_section.rb @@ -0,0 +1,11 @@ +module PreferenceSections + class UserGuideSection + def name + I18n.t('admin.contents.edit.user_guide') + end + + def preferences + [:user_guide_link] + end + end +end diff --git a/app/views/admin/shared/_user_guide_link.html.haml b/app/views/admin/shared/_user_guide_link.html.haml index cd1b0dba86..26aafc8bc7 100644 --- a/app/views/admin/shared/_user_guide_link.html.haml +++ b/app/views/admin/shared/_user_guide_link.html.haml @@ -1 +1 @@ -= button_link_to t('.user_guide'), "http://www.openfoodnetwork.org/platform/user-guide/", icon: 'icon-external-link', target: '_blank' += button_link_to t('.user_guide'), ContentConfig.user_guide_link, icon: 'icon-external-link', target: '_blank' diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index ab804b268e..1d68f48116 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -6,7 +6,7 @@ = "#{t(:email_registered)} #{ Spree::Config.site_name }!" %p - = t :email_userguide_html, link: link_to('Open Food Network User Guide', 'http://www.openfoodnetwork.org/platform/user-guide/') + = t :email_userguide_html, link: link_to('Open Food Network User Guide', ContentConfig.user_guide_link) %p = t :email_admin_html, link: link_to('Admin Panel', spree.admin_url) diff --git a/config/locales/en.yml b/config/locales/en.yml index f34706ca29..f1b386a03a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -431,6 +431,7 @@ en: main_links: Main Menu Links footer_and_external_links: Footer and External Links your_content: Your content + user_guide: User Guide enterprise_fees: index: @@ -1200,6 +1201,8 @@ en: footer_links_md: "Links" footer_about_url: "About URL" + user_guide_link: "User Guide Link" + name: Name first_name: First Name last_name: Last Name diff --git a/spec/features/admin/content_spec.rb b/spec/features/admin/content_spec.rb index 073ae35e3c..daf4cb516a 100644 --- a/spec/features/admin/content_spec.rb +++ b/spec/features/admin/content_spec.rb @@ -18,23 +18,35 @@ feature %q{ fill_in 'footer_twitter_url', with: 'http://twitter.com/me' fill_in 'footer_links_md', with: '[markdown link](/)' click_button 'Update' - page.should have_content 'Your content has been successfully updated!' + expect(page).to have_content 'Your content has been successfully updated!' visit root_path # Then social media icons are only shown if they have a value - page.should_not have_selector 'i.ofn-i_044-facebook' - page.should have_selector 'i.ofn-i_041-twitter' + expect(page).not_to have_selector 'i.ofn-i_044-facebook' + expect(page).to have_selector 'i.ofn-i_041-twitter' # And markdown is rendered - page.should have_link 'markdown link' + expect(page).to have_link 'markdown link' end scenario "uploading logos" do attach_file 'logo', "#{Rails.root}/app/assets/images/logo-white.png" click_button 'Update' - page.should have_content 'Your content has been successfully updated!' + expect(page).to have_content 'Your content has been successfully updated!' - ContentConfig.logo.to_s.should include "logo-white" + expect(ContentConfig.logo.to_s).to include "logo-white" + end + + scenario "setting user guide link" do + fill_in 'user_guide_link', with: 'http://www.openfoodnetwork.org/platform/user-guide/' + click_button 'Update' + + expect(page).to have_content 'Your content has been successfully updated!' + + visit spree.admin_path + + expect(page).to have_link('User Guide', href: 'http://www.openfoodnetwork.org/platform/user-guide/') + expect(find_link('User Guide')[:target]).to eq('_blank') end end From 409f084bd468813b593e94677ea80dd9fe94155c Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Sun, 2 Sep 2018 14:59:58 +0100 Subject: [PATCH 067/190] Added matomo opt out iframe to cookies policy page --- .../pages/cookies_policy_modal.css.scss | 5 ++ app/helpers/cookies_policy_helper.rb | 13 ++++ .../cookies_policy.html.haml | 6 ++ config/locales/en.yml | 1 + spec/features/consumer/cookies_spec.rb | 71 ++++++++++++++----- spec/helpers/cookies_policy_helper_spec.rb | 42 +++++++++++ 6 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 spec/helpers/cookies_policy_helper_spec.rb diff --git a/app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss b/app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss index 942a4396e0..8cf87b1ebb 100644 --- a/app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss +++ b/app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss @@ -16,6 +16,11 @@ } } + iframe { + border: 0; + width: 100%; + } + &.fade { -ms-transform: translate(0, 0) !important; -webkit-transform: translate(0, 0) !important; diff --git a/app/helpers/cookies_policy_helper.rb b/app/helpers/cookies_policy_helper.rb index 2afd28d9db..98db25d698 100644 --- a/app/helpers/cookies_policy_helper.rb +++ b/app/helpers/cookies_policy_helper.rb @@ -5,4 +5,17 @@ module CookiesPolicyHelper cookie_desc: cookie_desc, cookie_domain: cookie_domain } end + + def matomo_iframe_src + Spree::Config.matomo_url\ + "/index.php?module=CoreAdminHome&action=optOut"\ + "&language=#{locale_language}"\ + "&backgroundColor=&fontColor=222222&fontSize=16px&fontFamily=%22Roboto%22%2C%20Arial%2C%20sans-serif" + end + + # removes country from locale if needed + # for example, both locales en and en_GB return language en + def locale_language + I18n.locale[0..1] + end end diff --git a/app/views/angular_templates/cookies_policy.html.haml b/app/views/angular_templates/cookies_policy.html.haml index e43d1ded9e..982ba89d53 100644 --- a/app/views/angular_templates/cookies_policy.html.haml +++ b/app/views/angular_templates/cookies_policy.html.haml @@ -69,6 +69,12 @@ = render_cookie_entry( "_pk_hsr, _pk_cvar, _pk_id and _pk_ses", t( "legal.cookies_policy.cookie_matomo_heatmap_desc" ) ) = render_cookie_entry( "piwik_ignore, _pk_cvar, _pk_id and _pk_ses", t( "legal.cookies_policy.cookie_matomo_ignore_desc" ) ) + - if Spree::Config.cookies_policy_matomo_section && Spree::Config.matomo_url.present? + %p + = t 'legal.cookies_policy.statistics_cookies_matomo_optout' + %p + %iframe{ src: matomo_iframe_src } + %h2 = t 'legal.cookies_policy.disabling_cookies_header' %p diff --git a/config/locales/en.yml b/config/locales/en.yml index 7d3ce3b831..bae387e957 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1299,6 +1299,7 @@ en: statistics_cookies_desc: "The following are not strictly necessary, but help to provide you with the best user experience by allowing us to analyse user behaviour, identify which features you use most, or don’t use, understand user experience issues, etc." statistics_cookies_analytics_desc_html: "To collect and analyse platform usage data, we use Google Analytics, as it was the default service connected with Spree (the e-commerce open source software that we built on) but our vision is to switch to Matomo (ex Piwik, open source analytics tool that is GDPR compliant and protects your privacy) as soon as we can." statistics_cookies_matomo_desc_html: "To collect and analyse platform usage data, we use Matomo (ex Piwik), an open source analytics tool that is GDPR compliant and protects your privacy." + statistics_cookies_matomo_optout: "Do you want to opt-out of Matomo analytics? We don’t collect any personal data, and Matomo helps us to improve our service, but we respect your choice :-)" cookie_analytics_utma_desc: "Used to distinguish users and sessions. The cookie is created when the javascript library executes and no existing __utma cookies exists. The cookie is updated every time data is sent to Google Analytics." cookie_analytics_utmt_desc: "Used to throttle request rate." cookie_analytics_utmb_desc: "Used to determine new sessions/visits. The cookie is created when the javascript library executes and no existing __utmb cookies exists. The cookie is updated every time data is sent to Google Analytics." diff --git a/spec/features/consumer/cookies_spec.rb b/spec/features/consumer/cookies_spec.rb index c2dbe5a078..d86b89f4ad 100644 --- a/spec/features/consumer/cookies_spec.rb +++ b/spec/features/consumer/cookies_spec.rb @@ -10,7 +10,7 @@ feature "Cookies", js: true do Spree::Config[:cookies_consent_banner_toggle] = original_banner_toggle end - describe "in the homepage" do + context "in the homepage" do before do Spree::Config[:cookies_consent_banner_toggle] = true visit_root_path_and_wait @@ -46,7 +46,7 @@ feature "Cookies", js: true do end end - describe "in product listing page" do + context "in product listing page" do before do Spree::Config[:cookies_consent_banner_toggle] = true end @@ -57,7 +57,7 @@ feature "Cookies", js: true do end end - describe "disabled in the settings" do + context "disabled in the settings" do scenario "it is not showing" do Spree::Config[:cookies_consent_banner_toggle] = false visit root_path @@ -70,38 +70,59 @@ feature "Cookies", js: true do # keeps config unchanged around do |example| - original_config_value = Spree::Config[:cookies_policy_matomo_section] + original_matomo_config = Spree::Config[:cookies_policy_matomo_section] + original_matomo_url_config = Spree::Config[:matomo_url] example.run - Spree::Config[:cookies_policy_matomo_section] = original_config_value + Spree::Config[:cookies_policy_matomo_section] = original_matomo_config + Spree::Config[:matomo_url] = original_matomo_url_config end - scenario "showing session_id cookies description with correct instance domain" do + scenario "shows session_id cookies description with correct instance domain" do visit '/#/policies/cookies' expect(page).to have_content('_session_id') .and have_content('127.0.0.1') end - describe "with Matomo section configured" do - scenario "shows Matomo cookies details" do + context "without Matomo section configured" do + scenario "does not show Matomo cookies details and does not show Matomo optout text" do + Spree::Config[:cookies_policy_matomo_section] = false + visit_cookies_policy_page + expect(page).to have_no_content matomo_description_text + expect(page).to have_no_content matomo_opt_out_iframe + end + end + + context "with Matomo section configured" do + before do Spree::Config[:cookies_policy_matomo_section] = true - visit '/#/policies/cookies' + end + + scenario "shows Matomo cookies details" do + visit_cookies_policy_page expect(page).to have_content matomo_description_text end - end - describe "without Matomo section configured" do - scenario "does not show Matomo cookies details" do - Spree::Config[:cookies_policy_matomo_section] = false - visit '/#/policies/cookies' - expect(page).to have_no_content matomo_description_text + context "with Matomo integration enabled" do + scenario "shows Matomo optout iframe" do + Spree::Config[:matomo_url] = "https://0000.innocraft.cloud/" + visit_cookies_policy_page + expect(page).to have_content matomo_opt_out_iframe + expect(page).to have_selector("iframe") + end + end + + context "with Matomo integration disabled" do + scenario "does not show Matomo iframe" do + Spree::Config[:cookies_policy_matomo_section] = true + Spree::Config[:matomo_url] = "" + visit_cookies_policy_page + expect(page).to have_no_content matomo_opt_out_iframe + expect(page).to have_no_selector("iframe") + end end end end - def matomo_description_text - I18n.t('legal.cookies_policy.cookie_matomo_basics_desc') - end - def expect_visible_cookies_policy_page expect(page).to have_content I18n.t('legal.cookies_policy.header') end @@ -142,4 +163,16 @@ feature "Cookies", js: true do find("a.close-reveal-modal").click sleep 2 end + + def visit_cookies_policy_page + visit '/#/policies/cookies' + end + + def matomo_description_text + I18n.t('legal.cookies_policy.cookie_matomo_basics_desc') + end + + def matomo_opt_out_iframe + I18n.t('legal.cookies_policy.statistics_cookies_matomo_optout') + end end diff --git a/spec/helpers/cookies_policy_helper_spec.rb b/spec/helpers/cookies_policy_helper_spec.rb new file mode 100644 index 0000000000..2e4fbfa25d --- /dev/null +++ b/spec/helpers/cookies_policy_helper_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe CookiesPolicyHelper, type: :helper do + + # keeps global state unchanged + around do |example| + original_locale = I18n.locale + original_matomo_url = Spree::Config.matomo_url + example.run + Spree::Config.matomo_url = original_matomo_url + I18n.locale = original_locale + end + + describe "matomo optout iframe src" do + scenario "includes matomo URL" do + Spree::Config.matomo_url = "http://matomo.org/" + expect(helper.matomo_iframe_src).to include Spree::Config.matomo_url + end + + scenario "is not nil, when matomo url is nil" do + Spree::Config.matomo_url = nil + expect(helper.matomo_iframe_src).to_not eq nil + end + end + + describe "language from locale" do + scenario "when locale is the language" do + I18n.locale = "en" + expect(helper.locale_language).to eq "en" + end + + scenario "is empty when locale is empty" do + I18n.locale = "" + expect(helper.locale_language).to be_empty + end + + scenario "is only the language, when locale includes country" do + I18n.locale = "en_GB" + expect(helper.locale_language).to eq "en" + end + end +end From 478dd6807293fc361ab5833cc96c2a64485e6ed8 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Tue, 11 Sep 2018 17:25:11 +0100 Subject: [PATCH 068/190] In cookies policy helper, added string interpolation to variable to avoid variable (Spree::Config entry) to break the concatenation. Added unit test to validate the error case --- app/helpers/cookies_policy_helper.rb | 2 +- spec/helpers/cookies_policy_helper_spec.rb | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/helpers/cookies_policy_helper.rb b/app/helpers/cookies_policy_helper.rb index 98db25d698..5bcec92383 100644 --- a/app/helpers/cookies_policy_helper.rb +++ b/app/helpers/cookies_policy_helper.rb @@ -7,7 +7,7 @@ module CookiesPolicyHelper end def matomo_iframe_src - Spree::Config.matomo_url\ + "#{Spree::Config.matomo_url}"\ "/index.php?module=CoreAdminHome&action=optOut"\ "&language=#{locale_language}"\ "&backgroundColor=&fontColor=222222&fontSize=16px&fontFamily=%22Roboto%22%2C%20Arial%2C%20sans-serif" diff --git a/spec/helpers/cookies_policy_helper_spec.rb b/spec/helpers/cookies_policy_helper_spec.rb index 2e4fbfa25d..b09f578a32 100644 --- a/spec/helpers/cookies_policy_helper_spec.rb +++ b/spec/helpers/cookies_policy_helper_spec.rb @@ -12,9 +12,18 @@ describe CookiesPolicyHelper, type: :helper do end describe "matomo optout iframe src" do - scenario "includes matomo URL" do - Spree::Config.matomo_url = "http://matomo.org/" - expect(helper.matomo_iframe_src).to include Spree::Config.matomo_url + describe "when matomo url is set" do + before do + Spree::Config.matomo_url = "http://matomo.org/" + end + + scenario "includes the matomo URL" do + expect(helper.matomo_iframe_src).to include Spree::Config.matomo_url + end + + scenario "is not equal to the matomo URL" do + expect(helper.matomo_iframe_src).to_not eq Spree::Config.matomo_url + end end scenario "is not nil, when matomo url is nil" do From a447fe4f4085483e488bff3cb302123e9f688ac5 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Tue, 11 Sep 2018 16:58:12 +1000 Subject: [PATCH 069/190] Require lib file where needed We can't always rely on other parts of the code been loaded first. We need to declare dependencies so that they are always present. I just ran into this problem in my dev environment. --- app/controllers/admin/subscription_line_items_controller.rb | 1 + app/controllers/spree/admin/line_items_controller_decorator.rb | 2 ++ app/models/order_cycle.rb | 2 ++ app/models/spree/inventory_unit_decorator.rb | 2 ++ app/models/spree/line_item_decorator.rb | 1 + app/services/order_factory.rb | 2 ++ app/services/subscription_estimator.rb | 2 ++ lib/open_food_network/products_and_inventory_report_base.rb | 2 ++ lib/open_food_network/scope_variants_for_search.rb | 2 ++ lib/spree/core/controller_helpers/order_decorator.rb | 2 ++ 10 files changed, 18 insertions(+) diff --git a/app/controllers/admin/subscription_line_items_controller.rb b/app/controllers/admin/subscription_line_items_controller.rb index 5011129c5f..d981b71fe1 100644 --- a/app/controllers/admin/subscription_line_items_controller.rb +++ b/app/controllers/admin/subscription_line_items_controller.rb @@ -1,5 +1,6 @@ require 'open_food_network/permissions' require 'open_food_network/order_cycle_permissions' +require 'open_food_network/scope_variant_to_hub' module Admin class SubscriptionLineItemsController < ResourceController diff --git a/app/controllers/spree/admin/line_items_controller_decorator.rb b/app/controllers/spree/admin/line_items_controller_decorator.rb index 7210a6abb1..3ef85234df 100644 --- a/app/controllers/spree/admin/line_items_controller_decorator.rb +++ b/app/controllers/spree/admin/line_items_controller_decorator.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_variant_to_hub' + Spree::Admin::LineItemsController.class_eval do prepend_before_filter :load_order, except: :index around_filter :apply_enterprise_fees_with_lock, only: :update diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 4d8f443a68..7948d1ed43 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_variant_to_hub' + class OrderCycle < ActiveRecord::Base belongs_to :coordinator, :class_name => 'Enterprise' diff --git a/app/models/spree/inventory_unit_decorator.rb b/app/models/spree/inventory_unit_decorator.rb index 9868596b41..f3f5481a61 100644 --- a/app/models/spree/inventory_unit_decorator.rb +++ b/app/models/spree/inventory_unit_decorator.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_variant_to_hub' + module Spree InventoryUnit.class_eval do def self.assign_opening_inventory(order) diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index 6f74e19530..acc4e308b6 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -1,3 +1,4 @@ +require 'open_food_network/scope_variant_to_hub' require 'open_food_network/variant_and_line_item_naming' Spree::LineItem.class_eval do diff --git a/app/services/order_factory.rb b/app/services/order_factory.rb index fdef7b6b66..68e7224158 100644 --- a/app/services/order_factory.rb +++ b/app/services/order_factory.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_variant_to_hub' + # Builds orders based on a set of attributes # There are some idiosyncracies in the order creation process, # and it is nice to have them dealt with in one place. diff --git a/app/services/subscription_estimator.rb b/app/services/subscription_estimator.rb index 5abe388029..8e69e310ac 100644 --- a/app/services/subscription_estimator.rb +++ b/app/services/subscription_estimator.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_variant_to_hub' + # Responsible for estimating prices and fees for subscriptions # Used by SubscriptionForm as part of the create/update process # The values calculated here are intended to be persisted in the db diff --git a/lib/open_food_network/products_and_inventory_report_base.rb b/lib/open_food_network/products_and_inventory_report_base.rb index 121df3221e..15110047d9 100644 --- a/lib/open_food_network/products_and_inventory_report_base.rb +++ b/lib/open_food_network/products_and_inventory_report_base.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_variant_to_hub' + module OpenFoodNetwork class ProductsAndInventoryReportBase attr_reader :params diff --git a/lib/open_food_network/scope_variants_for_search.rb b/lib/open_food_network/scope_variants_for_search.rb index d04502dd4c..9befef5304 100644 --- a/lib/open_food_network/scope_variants_for_search.rb +++ b/lib/open_food_network/scope_variants_for_search.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_variant_to_hub' + # Used to return a set of variants which match the criteria provided # A query string is required, which will be match to the name and/or SKU of a product # Further restrictions on the schedule, order_cycle or distributor through which the diff --git a/lib/spree/core/controller_helpers/order_decorator.rb b/lib/spree/core/controller_helpers/order_decorator.rb index dc38e861e6..eb9ad451eb 100644 --- a/lib/spree/core/controller_helpers/order_decorator.rb +++ b/lib/spree/core/controller_helpers/order_decorator.rb @@ -1,3 +1,5 @@ +require 'open_food_network/scope_variant_to_hub' + Spree::Core::ControllerHelpers::Order.class_eval do def current_order_with_scoped_variants(create_order_if_necessary = false) order = current_order_without_scoped_variants(create_order_if_necessary) From d7b19750e5c5d145decc4fb083c6be92b043244a Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Tue, 11 Sep 2018 17:50:34 +1000 Subject: [PATCH 070/190] Add spec for GH issue #2655 --- spec/features/admin/payments_spec.rb | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 spec/features/admin/payments_spec.rb diff --git a/spec/features/admin/payments_spec.rb b/spec/features/admin/payments_spec.rb new file mode 100644 index 0000000000..2a41484813 --- /dev/null +++ b/spec/features/admin/payments_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +feature ' + As an admin + I want to manage payments +' do + include AuthenticationWorkflow + + let(:order) { create(:completed_order_with_fees) } + + scenario "visiting the payment form" do + quick_login_as_admin + + visit spree.new_admin_order_payment_path order + + expect(page).to have_content "New Payment" + end + + context "with sensitive payment fee" do + let(:payment_method) { order.distributor.payment_methods.first } + + before do + # This calculator doesn't handle a `nil` order well. + # That has been useful in finding bugs. ;-) + payment_method.calculator = Spree::Calculator::FlatPercentItemTotal.new + payment_method.save! + end + + scenario "visiting the payment form" do + pending "fix usage of the PaymentMethodSerializer" + quick_login_as_admin + + visit spree.new_admin_order_payment_path order + + expect(page).to have_content "New Payment" + end + end +end From 81f60aab46b27789f8f73da92d57826ed806e4fc Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 13 Sep 2018 11:59:34 +1000 Subject: [PATCH 071/190] Make PaymentMethod serialisable with fees https://github.com/openfoodfoundation/openfoodnetwork/issues/2655 --- app/views/spree/admin/payments/_form.html.erb | 2 +- spec/features/admin/payments_spec.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/spree/admin/payments/_form.html.erb b/app/views/spree/admin/payments/_form.html.erb index e46b2febcd..e075d1288a 100644 --- a/app/views/spree/admin/payments/_form.html.erb +++ b/app/views/spree/admin/payments/_form.html.erb @@ -1,5 +1,5 @@ <%= admin_inject_json "admin.payments", "currentOrderNumber", @order.number %> -<%= admin_inject_json_ams_array "admin.payments", "paymentMethods", @payment_methods, Api::PaymentMethodSerializer %> +<%= admin_inject_json_ams_array "admin.payments", "paymentMethods", @payment_methods, Api::PaymentMethodSerializer, current_order: @order %>
    diff --git a/spec/features/admin/payments_spec.rb b/spec/features/admin/payments_spec.rb index 2a41484813..0a667e9833 100644 --- a/spec/features/admin/payments_spec.rb +++ b/spec/features/admin/payments_spec.rb @@ -27,7 +27,6 @@ feature ' end scenario "visiting the payment form" do - pending "fix usage of the PaymentMethodSerializer" quick_login_as_admin visit spree.new_admin_order_payment_path order From b676bfdcc886a34b808f8dcd1eebf35e065acfb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Thu, 13 Sep 2018 02:34:11 +0000 Subject: [PATCH 072/190] Bump jwt from 1.5.4 to 1.5.6 Bumps [jwt](https://github.com/jwt/ruby-jwt) from 1.5.4 to 1.5.6. - [Release notes](https://github.com/jwt/ruby-jwt/releases) - [Changelog](https://github.com/jwt/ruby-jwt/blob/master/CHANGELOG.md) - [Commits](https://github.com/jwt/ruby-jwt/compare/v1.5.4...v1.5.6) Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9ec89f5f31..6ae9983d4e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -453,7 +453,7 @@ GEM json_spec (1.1.5) multi_json (~> 1.0) rspec (>= 2.0, < 4.0) - jwt (1.5.4) + jwt (1.5.6) kaminari (0.13.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) From 2e635f94f626c9b1dcc1f7614e2fbef97449505a Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 13 Sep 2018 17:18:03 +1000 Subject: [PATCH 073/190] Make job queuing more robust and efficient --- .../products_cache_refreshment.rb | 32 ++++++++----------- .../products_cache_refreshment_spec.rb | 1 + 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/open_food_network/products_cache_refreshment.rb b/lib/open_food_network/products_cache_refreshment.rb index 276734570f..17efd2acd5 100644 --- a/lib/open_food_network/products_cache_refreshment.rb +++ b/lib/open_food_network/products_cache_refreshment.rb @@ -17,31 +17,25 @@ module OpenFoodNetwork class ProductsCacheRefreshment def self.refresh(distributor, order_cycle) - unless pending_job? distributor, order_cycle - enqueue_job distributor, order_cycle - end + job = refresh_job(distributor, order_cycle) + enqueue_job(job) unless pending_job?(job) 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? + def self.refresh_job(distributor, order_cycle) + RefreshProductsCacheJob.new(distributor.id, order_cycle.id) end - def self.enqueue_job(distributor, order_cycle) - Delayed::Job.enqueue RefreshProductsCacheJob.new(distributor.id, order_cycle.id), priority: 10 + def self.pending_job?(job) + Delayed::Job. + where(locked_at: nil). + where(handler: job.to_yaml). + exists? + end + + def self.enqueue_job(job) + Delayed::Job.enqueue job, priority: 10 end end end diff --git a/spec/lib/open_food_network/products_cache_refreshment_spec.rb b/spec/lib/open_food_network/products_cache_refreshment_spec.rb index f65b81346e..74998928d5 100644 --- a/spec/lib/open_food_network/products_cache_refreshment_spec.rb +++ b/spec/lib/open_food_network/products_cache_refreshment_spec.rb @@ -1,3 +1,4 @@ +require 'spec_helper' require 'open_food_network/products_cache_refreshment' module OpenFoodNetwork From ec953e1db0a8f704bd5fbde5d50a06b4dd967a91 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 13 Sep 2018 17:22:01 +1000 Subject: [PATCH 074/190] Style cache refreshment class --- .rubocop_todo.yml | 6 ----- .../products_cache_refreshment.rb | 26 ++++++++++--------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index eaff617a86..7798d41309 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -185,7 +185,6 @@ Layout/EmptyLines: - 'lib/open_food_network/order_cycle_permissions.rb' - 'lib/open_food_network/products_cache.rb' - 'lib/open_food_network/products_cache_integrity_checker.rb' - - 'lib/open_food_network/products_cache_refreshment.rb' - 'lib/open_food_network/products_renderer.rb' - 'lib/open_food_network/property_merge.rb' - 'lib/open_food_network/reports/bulk_coop_report.rb' @@ -560,7 +559,6 @@ Layout/MultilineOperationIndentation: - 'app/models/variant_override_set.rb' - 'lib/open_food_network/accounts_and_billing_settings_validator.rb' - 'lib/open_food_network/order_cycle_permissions.rb' - - 'lib/open_food_network/products_cache_refreshment.rb' - 'lib/open_food_network/sales_tax_report.rb' - 'lib/open_food_network/users_and_enterprises_report.rb' @@ -986,7 +984,6 @@ Lint/IneffectiveAccessModifier: - 'app/models/variant_override.rb' - 'lib/open_food_network/feature_toggle.rb' - 'lib/open_food_network/products_cache.rb' - - 'lib/open_food_network/products_cache_refreshment.rb' - 'lib/open_food_network/property_merge.rb' - 'spec/lib/open_food_network/reports/report_spec.rb' @@ -1096,7 +1093,6 @@ Lint/UselessAccessModifier: - 'app/models/column_preference.rb' - 'lib/open_food_network/feature_toggle.rb' - 'lib/open_food_network/products_cache.rb' - - 'lib/open_food_network/products_cache_refreshment.rb' - 'lib/open_food_network/property_merge.rb' - 'lib/open_food_network/reports/bulk_coop_report.rb' - 'spec/lib/open_food_network/reports/report_spec.rb' @@ -1505,7 +1501,6 @@ Rails/TimeZone: - 'lib/open_food_network/users_and_enterprises_report.rb' - 'spec/controllers/api/statuses_controller_spec.rb' - 'spec/jobs/heartbeat_job_spec.rb' - - 'spec/lib/open_food_network/products_cache_refreshment_spec.rb' - 'spec/lib/open_food_network/products_cache_spec.rb' - 'spec/models/enterprise_relationship_spec.rb' - 'spec/models/variant_override_spec.rb' @@ -1861,7 +1856,6 @@ Style/GuardClause: - 'lib/open_food_network/accounts_and_billing_settings_validator.rb' - 'lib/open_food_network/order_cycle_form_applicator.rb' - 'lib/open_food_network/products_cache.rb' - - 'lib/open_food_network/products_cache_refreshment.rb' - 'lib/open_food_network/products_renderer.rb' - 'lib/open_food_network/rack_request_blocker.rb' - 'lib/open_food_network/variant_and_line_item_naming.rb' diff --git a/lib/open_food_network/products_cache_refreshment.rb b/lib/open_food_network/products_cache_refreshment.rb index 17efd2acd5..e9ebc67029 100644 --- a/lib/open_food_network/products_cache_refreshment.rb +++ b/lib/open_food_network/products_cache_refreshment.rb @@ -21,21 +21,23 @@ module OpenFoodNetwork enqueue_job(job) unless pending_job?(job) end - private + class << self + private - def self.refresh_job(distributor, order_cycle) - RefreshProductsCacheJob.new(distributor.id, order_cycle.id) - end + def refresh_job(distributor, order_cycle) + RefreshProductsCacheJob.new(distributor.id, order_cycle.id) + end - def self.pending_job?(job) - Delayed::Job. - where(locked_at: nil). - where(handler: job.to_yaml). - exists? - end + def pending_job?(job) + Delayed::Job. + where(locked_at: nil). + where(handler: job.to_yaml). + exists? + end - def self.enqueue_job(job) - Delayed::Job.enqueue job, priority: 10 + def enqueue_job(job) + Delayed::Job.enqueue job, priority: 10 + end end end end From 8dd0e01b8ee3975858f732d47863929782eaebcb Mon Sep 17 00:00:00 2001 From: Hugo Daniel Date: Thu, 13 Sep 2018 12:34:06 +0200 Subject: [PATCH 075/190] Use explicit syntax for section objects in preference_sections definition --- app/controllers/admin/contents_controller.rb | 14 ++++++++++---- app/models/content_configuration.rb | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/controllers/admin/contents_controller.rb b/app/controllers/admin/contents_controller.rb index 28f117c52c..5ac1be4f1d 100644 --- a/app/controllers/admin/contents_controller.rb +++ b/app/controllers/admin/contents_controller.rb @@ -24,10 +24,16 @@ module Admin private def preference_sections - Dir["app/models/preference_sections/*.rb"].map do |filename| - basename = 'PreferenceSections::' + File.basename(filename, '.rb').camelize - basename.constantize.new - end + [ + PreferenceSections::HeaderSection.new, + PreferenceSections::HomePageSection.new, + PreferenceSections::ProducerSignupPageSection.new, + PreferenceSections::HubSignupPageSection.new, + PreferenceSections::GroupSignupPageSection.new, + PreferenceSections::MainLinksSection.new, + PreferenceSections::FooterAndExternalLinksSection.new, + PreferenceSections::UserGuideSection.new + ] end end end diff --git a/app/models/content_configuration.rb b/app/models/content_configuration.rb index 1780389870..3e1e2b0939 100644 --- a/app/models/content_configuration.rb +++ b/app/models/content_configuration.rb @@ -71,5 +71,5 @@ EOS preference :footer_about_url, :string, default: "http://www.openfoodnetwork.org/ofn-local/open-food-network-australia/" #User Guide - preference :user_guide_link, :string, default: 'http://www.openfoodnetwork.org/platform/user-guide/' + preference :user_guide_link, :string, default: 'https://guide.openfoodnetwork.org/' end From 05225aaba735d2b82a8fd3d0aae9b8f7372cc40e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 14 Sep 2018 06:29:22 +0000 Subject: [PATCH 076/190] [Security] Bump uglifier from 2.7.1 to 4.1.19 Bumps [uglifier](https://github.com/lautis/uglifier) from 2.7.1 to 4.1.19. **This update includes security fixes.** - [Release notes](https://github.com/lautis/uglifier/releases) - [Changelog](https://github.com/lautis/uglifier/blob/master/CHANGELOG.md) - [Commits](https://github.com/lautis/uglifier/compare/v2.7.1...v4.1.19) Signed-off-by: dependabot[bot] --- Gemfile.lock | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 25844f45e9..6f2939c41d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -272,7 +272,7 @@ GEM erubis (2.7.0) eventmachine (1.2.7) excon (0.62.0) - execjs (2.6.0) + execjs (2.7.0) factory_bot (4.8.2) activesupport (>= 3.0.0) factory_bot_rails (4.8.2) @@ -713,9 +713,8 @@ GEM railties (> 3.2.8, < 4.0.0) sprockets (>= 2.0.0) tzinfo (0.3.54) - uglifier (2.7.1) - execjs (>= 0.3.0) - json (>= 1.8.0) + uglifier (4.1.19) + execjs (>= 0.3.0, < 3) unicode-display_width (1.3.2) unicorn (4.9.0) kgio (~> 2.6) From ac85b9031573fae1f84b060e9381c9bfa2878e7f Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Sat, 15 Sep 2018 10:18:54 +1000 Subject: [PATCH 077/190] Clarify private class method declaration --- .../products_cache_refreshment.rb | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/open_food_network/products_cache_refreshment.rb b/lib/open_food_network/products_cache_refreshment.rb index e9ebc67029..3bcde00f8d 100644 --- a/lib/open_food_network/products_cache_refreshment.rb +++ b/lib/open_food_network/products_cache_refreshment.rb @@ -21,23 +21,22 @@ module OpenFoodNetwork enqueue_job(job) unless pending_job?(job) end - class << self - private - - def refresh_job(distributor, order_cycle) - RefreshProductsCacheJob.new(distributor.id, order_cycle.id) - end - - def pending_job?(job) - Delayed::Job. - where(locked_at: nil). - where(handler: job.to_yaml). - exists? - end - - def enqueue_job(job) - Delayed::Job.enqueue job, priority: 10 - end + def self.refresh_job(distributor, order_cycle) + RefreshProductsCacheJob.new(distributor.id, order_cycle.id) end + private_class_method :refresh_job + + def self.pending_job?(job) + Delayed::Job. + where(locked_at: nil). + where(handler: job.to_yaml). + exists? + end + private_class_method :pending_job? + + def self.enqueue_job(job) + Delayed::Job.enqueue job, priority: 10 + end + private_class_method :enqueue_job end end From 798a6ed39155c99b2c1d5b51263bf8d3320c3330 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Fri, 14 Sep 2018 01:15:59 +0800 Subject: [PATCH 078/190] Translate more text in OC filters --- app/views/admin/order_cycles/_filters.html.haml | 4 ++-- config/locales/en.yml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/admin/order_cycles/_filters.html.haml b/app/views/admin/order_cycles/_filters.html.haml index dad49a5232..7cb735dd8a 100644 --- a/app/views/admin/order_cycles/_filters.html.haml +++ b/app/views/admin/order_cycles/_filters.html.haml @@ -6,12 +6,12 @@ .filter_select.four.columns %label{ :for => 'involving_filter' }=t('.involving') %br - %input.ofn-select2.fullwidth{ :id => 'involving_filter', type: 'number', blank: "{id: 0, name: 'Any Enterprise'}", data: 'enterprises', ng: { model: 'involvingFilter' } } + %input.ofn-select2.fullwidth{ id: 'involving_filter', type: 'number', blank: "{id: 0, name: '#{j t(".any_enterprise")}'}", data: 'enterprises', ng: { model: 'involvingFilter' } } - if subscriptions_enabled? .filter_select.four.columns %label{ :for => 'schedule_filter' }=t('admin.order_cycles.index.schedule') %br - %input.ofn-select2.fullwidth{ :id => 'schedule_filter', type: 'number', blank: "{id: 0, name: 'Any Schedule'}", data: 'schedules', ng: { model: 'scheduleFilter' } } + %input.ofn-select2.fullwidth{ id: 'schedule_filter', type: 'number', blank: "{id: 0, name: '#{j t(".any_schedule")}'}", data: 'schedules', ng: { model: 'scheduleFilter' } } .two.columns   - else .six.columns   diff --git a/config/locales/en.yml b/config/locales/en.yml index d83479dce6..dcd939c097 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -897,6 +897,8 @@ en: filters: search_by_order_cycle_name: "Search by Order Cycle name..." involving: "Involving" + any_enterprise: "Any Enterprise" + any_schedule: "Any Schedule" form: incoming: Incoming supplier: Supplier From e577bcb46fcec3024aec9dda7f4cf11484f488f1 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Wed, 12 Sep 2018 12:49:16 +0100 Subject: [PATCH 079/190] Prepare angular controller and serialized data --- .../controllers/orders_controller.js.coffee | 6 +++++- .../admin/orders_controller_decorator.rb | 21 +++++++++---------- app/serializers/api/admin/order_serializer.rb | 2 +- app/views/spree/admin/orders/index.html.haml | 9 +++++++- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 060b23bed9..afe3e87d2e 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.orders").controller "ordersCtrl", ($scope, $compile, $attrs, shops, orderCycles) -> +angular.module("admin.orders").controller "ordersCtrl", ($scope, $compile, $attrs, Orders) -> $scope.$compile = $compile $scope.shops = shops $scope.orderCycles = orderCycles @@ -6,6 +6,10 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, $compile, $attr $scope.distributor_id = parseInt($attrs.ofnDistributorId) $scope.order_cycle_id = parseInt($attrs.ofnOrderCycleId) + $scope.orders = Orders.all + + Orders.index(per_page: 15) + $scope.validOrderCycle = (oc) -> $scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id) diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index 49f017930d..43d1ce16b8 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -100,18 +100,17 @@ Spree::Admin::OrdersController.class_eval do private def orders - if json_request? - @search = OpenFoodNetwork::Permissions.new(spree_current_user).editable_orders.ransack(params[:q]) - @search.result.reorder('id ASC') - else - @search = Spree::Order.accessible_by(current_ability, :index).ransack(params[:q]) + @search = if json_request? + OpenFoodNetwork::Permissions.new(spree_current_user).editable_orders.ransack(params[:q]) + else + Spree::Order.accessible_by(current_ability, :index).ransack(params[:q]) + end - # Replaced this search to filter orders to only show those distributed by current user (or all for admin user) - @search.result.includes([:user, :shipments, :payments]). - distributed_by_user(spree_current_user). - page(params[:page]). - per(params[:per_page] || Spree::Config[:orders_per_page]) - end + # Replaced this search to filter orders to only show those distributed by current user (or all for admin user) + @search.result.includes([:user, :shipments, :payments]). + distributed_by_user(spree_current_user). + page(params[:page]). + per(params[:per_page] || Spree::Config[:orders_per_page]) end def require_distributor_abn diff --git a/app/serializers/api/admin/order_serializer.rb b/app/serializers/api/admin/order_serializer.rb index 6f22ba1e94..a010636307 100644 --- a/app/serializers/api/admin/order_serializer.rb +++ b/app/serializers/api/admin/order_serializer.rb @@ -1,5 +1,5 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer - attributes :id, :number, :full_name, :email, :phone, :completed_at + attributes :id, :number, :full_name, :email, :phone, :completed_at, :payment_total has_one :distributor, serializer: Api::Admin::IdSerializer has_one :order_cycle, serializer: Api::Admin::IdSerializer diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 3b233424cc..68e1763d99 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -1,11 +1,18 @@ - content_for :page_title do = t(:listing_orders) + - content_for :page_actions do %li = button_link_to t(:new_order), new_admin_order_url, :icon => 'icon-plus', :id => 'admin_new_order' + = render partial: 'spree/admin/shared/order_sub_menu' + +- content_for :app_wrapper_attrs do + = "ng-app='admin.orders'" + - content_for :table_filter_title do = t(:search) + - content_for :table_filter do %div{"data-hook" => "admin_orders_index_search"} = search_form_for [:admin, @search] do |f| @@ -59,7 +66,7 @@ = button t(:filter_results), 'icon-search' - unless @orders.empty? - %table#listing_orders.index.responsive{"data-hook" => "", width: "100%", "ng-app" => "ofn.admin"} + %table#listing_orders.index.responsive{"data-hook" => "", width: "100%", "ng-controller" => "ordersCtrl"} %colgroup %col{style: "width: 10%"} %thead From d170a4d489346ab1662087f4716abbec76e9ad0f Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Tue, 7 Aug 2018 15:25:47 +0100 Subject: [PATCH 080/190] New domain Web (rails engine) with the following features extracted from the main app: - cookies banner - cookies policy page - cookies policy and privacy policy links in the footer --- .rubocop.yml | 2 ++ Gemfile | 2 ++ Gemfile.lock | 6 ++++ .../api/cookies_consent_controller.rb | 26 ---------------- app/services/cookies_consent.rb | 29 ----------------- app/views/layouts/darkswarm.html.haml | 2 ++ app/views/shared/_footer.html.haml | 2 +- config/routes.rb | 7 ++--- engines/web/README.md | 5 +++ engines/web/app/assets/javascripts/web/all.js | 13 ++++++++ .../cookies_banner_controller.js.coffee | 0 .../cookies_banner_directive.js.coffee | 0 .../cookies_banner_service.js.coffee | 2 +- .../cookies_policy_modal_controller.js.coffee | 0 .../cookies_policy_modal_directive.js.coffee | 0 .../cookies_policy_modal_service.js.coffee | 0 engines/web/app/assets/javascripts/web/web.js | 2 ++ .../web/app/assets/stylesheets/web/all.css | 13 ++++++++ .../web}/pages/cookies_banner.css.scss | 2 +- .../web}/pages/cookies_policy_modal.css.scss | 2 +- .../web/api/cookies_consent_controller.rb | 30 ++++++++++++++++++ .../controllers/web/application_controller.rb | 5 +++ .../_cookies_policy_entry.html.haml | 0 .../cookies_banner.html.haml | 0 .../cookies_policy.html.haml | 0 engines/web/config/routes.rb | 7 +++++ engines/web/lib/web.rb | 4 +++ engines/web/lib/web/cookies_consent.rb | 31 +++++++++++++++++++ engines/web/lib/web/engine.rb | 4 +++ engines/web/lib/web/version.rb | 3 ++ engines/web/spec/spec_helper.rb | 8 +++++ engines/web/web.gemspec | 13 ++++++++ 32 files changed, 157 insertions(+), 63 deletions(-) delete mode 100644 app/controllers/api/cookies_consent_controller.rb delete mode 100644 app/services/cookies_consent.rb create mode 100644 engines/web/README.md create mode 100644 engines/web/app/assets/javascripts/web/all.js rename {app/assets/javascripts/darkswarm => engines/web/app/assets/javascripts/web}/cookies_banner/cookies_banner_controller.js.coffee (100%) rename {app/assets/javascripts/darkswarm => engines/web/app/assets/javascripts/web}/cookies_banner/cookies_banner_directive.js.coffee (100%) rename {app/assets/javascripts/darkswarm => engines/web/app/assets/javascripts/web}/cookies_banner/cookies_banner_service.js.coffee (86%) rename {app/assets/javascripts/darkswarm => engines/web/app/assets/javascripts/web}/cookies_policy/cookies_policy_modal_controller.js.coffee (100%) rename {app/assets/javascripts/darkswarm => engines/web/app/assets/javascripts/web}/cookies_policy/cookies_policy_modal_directive.js.coffee (100%) rename {app/assets/javascripts/darkswarm => engines/web/app/assets/javascripts/web}/cookies_policy/cookies_policy_modal_service.js.coffee (100%) create mode 100644 engines/web/app/assets/javascripts/web/web.js create mode 100644 engines/web/app/assets/stylesheets/web/all.css rename {app/assets/stylesheets/darkswarm => engines/web/app/assets/stylesheets/web}/pages/cookies_banner.css.scss (87%) rename {app/assets/stylesheets/darkswarm => engines/web/app/assets/stylesheets/web}/pages/cookies_policy_modal.css.scss (86%) create mode 100644 engines/web/app/controllers/web/api/cookies_consent_controller.rb create mode 100644 engines/web/app/controllers/web/application_controller.rb rename {app => engines/web/app}/views/angular_templates/_cookies_policy_entry.html.haml (100%) rename {app/assets/javascripts/darkswarm/cookies_banner => engines/web/app/views/angular_templates}/cookies_banner.html.haml (100%) rename {app => engines/web/app}/views/angular_templates/cookies_policy.html.haml (100%) create mode 100644 engines/web/config/routes.rb create mode 100644 engines/web/lib/web.rb create mode 100644 engines/web/lib/web/cookies_consent.rb create mode 100644 engines/web/lib/web/engine.rb create mode 100644 engines/web/lib/web/version.rb create mode 100644 engines/web/spec/spec_helper.rb create mode 100644 engines/web/web.gemspec diff --git a/.rubocop.yml b/.rubocop.yml index 80d4c1bf7e..b49623c344 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,6 +16,8 @@ AllCops: - !ruby/regexp /old_and_unused\.rb$/ # The parser gem fails to parse this file with out current Ruby version. - 'spec/factories.rb' + # Excluding: inadequate Naming/FileName rule rejects GemFile name with camelcase + - 'engines/web/Gemfile' # OFN SETTINGS # Cop settings that have been agreed upon by the OFN community diff --git a/Gemfile b/Gemfile index 2b33c60993..2efad595c0 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,8 @@ gem 'i18n-js', '~> 3.0.0' # Patched version. See http://rubysec.com/advisories/CVE-2015-5312/. gem 'nokogiri', '>= 1.6.7.1' +gem 'web', path: './engines/web' + gem 'pg' gem 'spree', github: 'openfoodfoundation/spree', branch: 'step-6a', ref: '69db1c090f3711088d84b524f1b94d25e6d21616' gem 'spree_i18n', github: 'spree/spree_i18n', branch: '1-3-stable' diff --git a/Gemfile.lock b/Gemfile.lock index 25844f45e9..2f2701fd55 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -133,6 +133,11 @@ GIT activemodel (>= 3.0) railties (>= 3.0) +PATH + remote: engines/web + specs: + web (0.0.1) + GEM remote: https://rubygems.org/ specs: @@ -767,6 +772,7 @@ DEPENDENCIES capybara (>= 2.15.4) coffee-rails (~> 3.2.1) compass-rails + web! custom_error_message! daemons dalli diff --git a/app/controllers/api/cookies_consent_controller.rb b/app/controllers/api/cookies_consent_controller.rb deleted file mode 100644 index 89016c2066..0000000000 --- a/app/controllers/api/cookies_consent_controller.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Api - class CookiesConsentController < BaseController - include ActionController::Cookies - respond_to :json - - def show - render json: { cookies_consent: cookies_consent.exists? } - end - - def create - cookies_consent.set - show - end - - def destroy - cookies_consent.destroy - show - end - - private - - def cookies_consent - @cookies_consent ||= CookiesConsent.new(cookies, request.host) - end - end -end diff --git a/app/services/cookies_consent.rb b/app/services/cookies_consent.rb deleted file mode 100644 index 64c4dfa083..0000000000 --- a/app/services/cookies_consent.rb +++ /dev/null @@ -1,29 +0,0 @@ -class CookiesConsent - COOKIE_NAME = 'cookies_consent'.freeze - - def initialize(cookies, domain) - @cookies = cookies - @domain = domain - end - - def exists? - cookies.key?(COOKIE_NAME) - end - - def destroy - cookies.delete(COOKIE_NAME, domain: domain) - end - - def set - cookies[COOKIE_NAME] = { - value: COOKIE_NAME, - expires: 1.year.from_now, - domain: domain, - httponly: true - } - end - - private - - attr_reader :cookies, :domain -end diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index e3cd92951f..83173c16e3 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -19,6 +19,8 @@ %script{src: "//maps.googleapis.com/maps/api/js?libraries=places,geometry#{ ENV['GOOGLE_MAPS_API_KEY'] ? '&key=' + ENV['GOOGLE_MAPS_API_KEY'] : ''} "} = stylesheet_link_tag "darkswarm/all" = javascript_include_tag "darkswarm/all" + = stylesheet_link_tag "web/all" + = javascript_include_tag "web/all" = render "layouts/i18n_script" = render "layouts/bugherd_script" diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index 9c7126c999..21a9525e0a 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -140,7 +140,7 @@ = t '.footer_legal_text_html', {content_license: link_to('CC BY-SA 3.0', 'https://creativecommons.org/licenses/by-sa/3.0/'), code_license: link_to('AGPL 3', 'https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)' )} %p.text-small %div - - cookies_policy_link = link_to( t( '.footer_data_cookies_policy' ), '', 'cookies-policy-modal' => true, 'cookies-banner' => !CookiesConsent.new(cookies, request.host).exists? && Spree::Config.cookies_consent_banner_toggle) + - cookies_policy_link = link_to( t( '.footer_data_cookies_policy' ), '', 'cookies-policy-modal' => true, 'cookies-banner' => !Web::CookiesConsent.new(cookies, request.host).exists? && Spree::Config.cookies_consent_banner_toggle) - privacy_policy_link = link_to( t( '.footer_data_privacy_policy' ), Spree::Config.privacy_policy_url, :target => '_blank' ) - if Spree::Config.privacy_policy_url.present? = t '.footer_data_text_with_privacy_policy_html', {cookies_policy: cookies_policy_link.html_safe, privacy_policy: privacy_policy_link.html_safe } diff --git a/config/routes.rb b/config/routes.rb index 4f6125c8ac..21d2346933 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -109,10 +109,6 @@ Openfoodnetwork::Application.routes.draw do get :job_queue end - scope '/cookies' do - resource :consent, only: [:show, :create, :destroy], :controller => "cookies_consent" - end - resources :customers, only: [:index, :update] post '/product_images/:product_id', to: 'product_images#update_product_image' @@ -120,6 +116,9 @@ Openfoodnetwork::Application.routes.draw do get 'sitemap.xml', to: 'sitemap#index', defaults: { format: 'xml' } + # Mount Web engine routes + mount Web::Engine, :at => '/' + # Mount Spree's routes mount Spree::Core::Engine, :at => '/' end diff --git a/engines/web/README.md b/engines/web/README.md new file mode 100644 index 0000000000..87945856ef --- /dev/null +++ b/engines/web/README.md @@ -0,0 +1,5 @@ +# Web + +This is the rails engine for the Web domain. + +See our wiki for [more info about domains and engines in OFN](https://github.com/openfoodfoundation/openfoodnetwork/wiki/Tech-Doc:-How-OFN-is-organized-in-Domains-using-Rails-Engines). diff --git a/engines/web/app/assets/javascripts/web/all.js b/engines/web/app/assets/javascripts/web/all.js new file mode 100644 index 0000000000..15ebed9422 --- /dev/null +++ b/engines/web/app/assets/javascripts/web/all.js @@ -0,0 +1,13 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD +// GO AFTER THE REQUIRES BELOW. +// +//= require_tree . diff --git a/app/assets/javascripts/darkswarm/cookies_banner/cookies_banner_controller.js.coffee b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee similarity index 100% rename from app/assets/javascripts/darkswarm/cookies_banner/cookies_banner_controller.js.coffee rename to engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee diff --git a/app/assets/javascripts/darkswarm/cookies_banner/cookies_banner_directive.js.coffee b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_directive.js.coffee similarity index 100% rename from app/assets/javascripts/darkswarm/cookies_banner/cookies_banner_directive.js.coffee rename to engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_directive.js.coffee diff --git a/app/assets/javascripts/darkswarm/cookies_banner/cookies_banner_service.js.coffee b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_service.js.coffee similarity index 86% rename from app/assets/javascripts/darkswarm/cookies_banner/cookies_banner_service.js.coffee rename to engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_service.js.coffee index 139552a214..c9d2911f73 100644 --- a/app/assets/javascripts/darkswarm/cookies_banner/cookies_banner_service.js.coffee +++ b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_service.js.coffee @@ -4,7 +4,7 @@ Darkswarm.factory "CookiesBannerService", (Navigation, $modal, $location, Redire modalMessage: null isEnabled: false - open: (path, template = 'darkswarm/cookies_banner/cookies_banner.html') => + open: (path, template = 'angular-templates/cookies_banner.html') => return unless @isEnabled @modalInstance = $modal.open templateUrl: template diff --git a/app/assets/javascripts/darkswarm/cookies_policy/cookies_policy_modal_controller.js.coffee b/engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_controller.js.coffee similarity index 100% rename from app/assets/javascripts/darkswarm/cookies_policy/cookies_policy_modal_controller.js.coffee rename to engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_controller.js.coffee diff --git a/app/assets/javascripts/darkswarm/cookies_policy/cookies_policy_modal_directive.js.coffee b/engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_directive.js.coffee similarity index 100% rename from app/assets/javascripts/darkswarm/cookies_policy/cookies_policy_modal_directive.js.coffee rename to engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_directive.js.coffee diff --git a/app/assets/javascripts/darkswarm/cookies_policy/cookies_policy_modal_service.js.coffee b/engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_service.js.coffee similarity index 100% rename from app/assets/javascripts/darkswarm/cookies_policy/cookies_policy_modal_service.js.coffee rename to engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_service.js.coffee diff --git a/engines/web/app/assets/javascripts/web/web.js b/engines/web/app/assets/javascripts/web/web.js new file mode 100644 index 0000000000..dee720facd --- /dev/null +++ b/engines/web/app/assets/javascripts/web/web.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/engines/web/app/assets/stylesheets/web/all.css b/engines/web/app/assets/stylesheets/web/all.css new file mode 100644 index 0000000000..3192ec897b --- /dev/null +++ b/engines/web/app/assets/stylesheets/web/all.css @@ -0,0 +1,13 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the top of the + * compiled file, but it's generally better to create a new file per style scope. + * + *= require_self + *= require_tree . + */ diff --git a/app/assets/stylesheets/darkswarm/pages/cookies_banner.css.scss b/engines/web/app/assets/stylesheets/web/pages/cookies_banner.css.scss similarity index 87% rename from app/assets/stylesheets/darkswarm/pages/cookies_banner.css.scss rename to engines/web/app/assets/stylesheets/web/pages/cookies_banner.css.scss index 6223220977..afd0d73d43 100644 --- a/app/assets/stylesheets/darkswarm/pages/cookies_banner.css.scss +++ b/engines/web/app/assets/stylesheets/web/pages/cookies_banner.css.scss @@ -1,4 +1,4 @@ -@import '../branding'; +@import '../../../../../../../app/assets/stylesheets/darkswarm/branding'; .cookies-banner { background: $dark-grey; diff --git a/app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss b/engines/web/app/assets/stylesheets/web/pages/cookies_policy_modal.css.scss similarity index 86% rename from app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss rename to engines/web/app/assets/stylesheets/web/pages/cookies_policy_modal.css.scss index 8cf87b1ebb..2279bf6983 100644 --- a/app/assets/stylesheets/darkswarm/pages/cookies_policy_modal.css.scss +++ b/engines/web/app/assets/stylesheets/web/pages/cookies_policy_modal.css.scss @@ -1,4 +1,4 @@ -@import '../branding'; +@import '../../../../../../../app/assets/stylesheets/darkswarm/branding'; .cookies-policy-modal { background: $disabled-light; diff --git a/engines/web/app/controllers/web/api/cookies_consent_controller.rb b/engines/web/app/controllers/web/api/cookies_consent_controller.rb new file mode 100644 index 0000000000..e0ea13bafe --- /dev/null +++ b/engines/web/app/controllers/web/api/cookies_consent_controller.rb @@ -0,0 +1,30 @@ +require_dependency 'web/cookies_consent' + +module Web + module Api + class CookiesConsentController < BaseController + include ActionController::Cookies + respond_to :json + + def show + render json: { cookies_consent: cookies_consent.exists? } + end + + def create + cookies_consent.set + show + end + + def destroy + cookies_consent.destroy + show + end + + private + + def cookies_consent + @cookies_consent ||= Web::CookiesConsent.new(cookies, request.host) + end + end + end +end diff --git a/engines/web/app/controllers/web/application_controller.rb b/engines/web/app/controllers/web/application_controller.rb new file mode 100644 index 0000000000..410b0ae531 --- /dev/null +++ b/engines/web/app/controllers/web/application_controller.rb @@ -0,0 +1,5 @@ +module Web + class ApplicationController < ActionController::Base + protect_from_forgery with: :exception + end +end diff --git a/app/views/angular_templates/_cookies_policy_entry.html.haml b/engines/web/app/views/angular_templates/_cookies_policy_entry.html.haml similarity index 100% rename from app/views/angular_templates/_cookies_policy_entry.html.haml rename to engines/web/app/views/angular_templates/_cookies_policy_entry.html.haml diff --git a/app/assets/javascripts/darkswarm/cookies_banner/cookies_banner.html.haml b/engines/web/app/views/angular_templates/cookies_banner.html.haml similarity index 100% rename from app/assets/javascripts/darkswarm/cookies_banner/cookies_banner.html.haml rename to engines/web/app/views/angular_templates/cookies_banner.html.haml diff --git a/app/views/angular_templates/cookies_policy.html.haml b/engines/web/app/views/angular_templates/cookies_policy.html.haml similarity index 100% rename from app/views/angular_templates/cookies_policy.html.haml rename to engines/web/app/views/angular_templates/cookies_policy.html.haml diff --git a/engines/web/config/routes.rb b/engines/web/config/routes.rb new file mode 100644 index 0000000000..e9143b1c7d --- /dev/null +++ b/engines/web/config/routes.rb @@ -0,0 +1,7 @@ +Web::Engine.routes.draw do + namespace :api do + scope '/cookies' do + resource :consent, only: [:show, :create, :destroy], controller: "cookies_consent" + end + end +end diff --git a/engines/web/lib/web.rb b/engines/web/lib/web.rb new file mode 100644 index 0000000000..613b763014 --- /dev/null +++ b/engines/web/lib/web.rb @@ -0,0 +1,4 @@ +require "web/engine" + +module Web +end diff --git a/engines/web/lib/web/cookies_consent.rb b/engines/web/lib/web/cookies_consent.rb new file mode 100644 index 0000000000..64f6c0c825 --- /dev/null +++ b/engines/web/lib/web/cookies_consent.rb @@ -0,0 +1,31 @@ +module Web + class CookiesConsent + COOKIE_NAME = 'cookies_consent'.freeze + + def initialize(cookies, domain) + @cookies = cookies + @domain = domain + end + + def exists? + cookies.key?(COOKIE_NAME) + end + + def destroy + cookies.delete(COOKIE_NAME, domain: domain) + end + + def set + cookies[COOKIE_NAME] = { + value: COOKIE_NAME, + expires: 1.year.from_now, + domain: domain, + httponly: true + } + end + + private + + attr_reader :cookies, :domain + end +end diff --git a/engines/web/lib/web/engine.rb b/engines/web/lib/web/engine.rb new file mode 100644 index 0000000000..11a353a3fd --- /dev/null +++ b/engines/web/lib/web/engine.rb @@ -0,0 +1,4 @@ +module Web + class Engine < ::Rails::Engine + end +end diff --git a/engines/web/lib/web/version.rb b/engines/web/lib/web/version.rb new file mode 100644 index 0000000000..ae51029b5c --- /dev/null +++ b/engines/web/lib/web/version.rb @@ -0,0 +1,3 @@ +module Web + VERSION = "0.0.1".freeze +end diff --git a/engines/web/spec/spec_helper.rb b/engines/web/spec/spec_helper.rb new file mode 100644 index 0000000000..3306063166 --- /dev/null +++ b/engines/web/spec/spec_helper.rb @@ -0,0 +1,8 @@ +ENV["RAILS_ENV"] = "test" + +require File.expand_path("dummy/config/environment.rb", __dir__) +require "rails/test_help" + +Rails.backtrace_cleaner.remove_silencers! + +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } diff --git a/engines/web/web.gemspec b/engines/web/web.gemspec new file mode 100644 index 0000000000..f44ee68909 --- /dev/null +++ b/engines/web/web.gemspec @@ -0,0 +1,13 @@ +$LOAD_PATH.push File.expand_path('lib', __dir__) + +require "web/version" + +Gem::Specification.new do |s| + s.name = "web" + s.version = Web::VERSION + s.authors = ["developers@ofn"] + s.summary = "Web domain of the OFN solution." + + s.files = Dir["{app,config,db,lib}/**/*"] + ["LICENSE.txt", "Rakefile", "README.rdoc"] + s.test_files = Dir["test/**/*"] +end From dc5eb6448e5d76ea579130d8584f55770aaefce5 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 17 Sep 2018 12:45:06 +0100 Subject: [PATCH 081/190] Change web/all.css from sprockets to SASS and include web/all.css through darkswarm/all.css --- app/assets/stylesheets/darkswarm/all.scss | 1 + app/views/layouts/darkswarm.html.haml | 1 - engines/web/app/assets/stylesheets/web/all.css | 13 ------------- engines/web/app/assets/stylesheets/web/all.css.scss | 2 ++ .../stylesheets/web/pages/cookies_banner.css.scss | 2 +- .../web/pages/cookies_policy_modal.css.scss | 2 +- 6 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 engines/web/app/assets/stylesheets/web/all.css create mode 100644 engines/web/app/assets/stylesheets/web/all.css.scss diff --git a/app/assets/stylesheets/darkswarm/all.scss b/app/assets/stylesheets/darkswarm/all.scss index b3f5433d8e..2f73f5e796 100644 --- a/app/assets/stylesheets/darkswarm/all.scss +++ b/app/assets/stylesheets/darkswarm/all.scss @@ -12,6 +12,7 @@ @import 'base/*'; @import '*'; @import 'pages/*'; +@import '../web/all'; ofn-modal { display: block; diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 83173c16e3..79d78c67ea 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -19,7 +19,6 @@ %script{src: "//maps.googleapis.com/maps/api/js?libraries=places,geometry#{ ENV['GOOGLE_MAPS_API_KEY'] ? '&key=' + ENV['GOOGLE_MAPS_API_KEY'] : ''} "} = stylesheet_link_tag "darkswarm/all" = javascript_include_tag "darkswarm/all" - = stylesheet_link_tag "web/all" = javascript_include_tag "web/all" = render "layouts/i18n_script" diff --git a/engines/web/app/assets/stylesheets/web/all.css b/engines/web/app/assets/stylesheets/web/all.css deleted file mode 100644 index 3192ec897b..0000000000 --- a/engines/web/app/assets/stylesheets/web/all.css +++ /dev/null @@ -1,13 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the top of the - * compiled file, but it's generally better to create a new file per style scope. - * - *= require_self - *= require_tree . - */ diff --git a/engines/web/app/assets/stylesheets/web/all.css.scss b/engines/web/app/assets/stylesheets/web/all.css.scss new file mode 100644 index 0000000000..53fb36eef5 --- /dev/null +++ b/engines/web/app/assets/stylesheets/web/all.css.scss @@ -0,0 +1,2 @@ +@import 'web/pages/cookies_banner'; +@import 'web/pages/cookies_policy_modal'; diff --git a/engines/web/app/assets/stylesheets/web/pages/cookies_banner.css.scss b/engines/web/app/assets/stylesheets/web/pages/cookies_banner.css.scss index afd0d73d43..a655c513a8 100644 --- a/engines/web/app/assets/stylesheets/web/pages/cookies_banner.css.scss +++ b/engines/web/app/assets/stylesheets/web/pages/cookies_banner.css.scss @@ -1,4 +1,4 @@ -@import '../../../../../../../app/assets/stylesheets/darkswarm/branding'; +@import 'darkswarm/branding'; .cookies-banner { background: $dark-grey; diff --git a/engines/web/app/assets/stylesheets/web/pages/cookies_policy_modal.css.scss b/engines/web/app/assets/stylesheets/web/pages/cookies_policy_modal.css.scss index 2279bf6983..490fe6e529 100644 --- a/engines/web/app/assets/stylesheets/web/pages/cookies_policy_modal.css.scss +++ b/engines/web/app/assets/stylesheets/web/pages/cookies_policy_modal.css.scss @@ -1,4 +1,4 @@ -@import '../../../../../../../app/assets/stylesheets/darkswarm/branding'; +@import 'darkswarm/branding'; .cookies-policy-modal { background: $disabled-light; From bded530137c2cb7ec85bdea06e17bc737721560c Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 17 Sep 2018 13:06:19 +0100 Subject: [PATCH 082/190] Moved web api endpoint from /api to /web/api --- .../cookies_banner/cookies_banner_controller.js.coffee | 2 +- engines/web/config/routes.rb | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee index a35951943a..d9444fbd78 100644 --- a/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee +++ b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee @@ -1,6 +1,6 @@ Darkswarm.controller "CookiesBannerCtrl", ($scope, CookiesBannerService, $http, $window)-> $scope.acceptCookies = -> - $http.post('/api/cookies/consent') + $http.post('/web/api/cookies/consent') CookiesBannerService.close() CookiesBannerService.disable() diff --git a/engines/web/config/routes.rb b/engines/web/config/routes.rb index e9143b1c7d..26a1a21690 100644 --- a/engines/web/config/routes.rb +++ b/engines/web/config/routes.rb @@ -1,7 +1,9 @@ Web::Engine.routes.draw do - namespace :api do - scope '/cookies' do - resource :consent, only: [:show, :create, :destroy], controller: "cookies_consent" + namespace :web do + namespace :api do + scope '/cookies' do + resource :consent, only: [:show, :create, :destroy], controller: "cookies_consent" + end end end end From 95f2f92cf3a6d5fb7d9b7391315c384fef16fdbc Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 17 Sep 2018 13:42:48 +0100 Subject: [PATCH 083/190] Extracted cookies footer links from view to footer links helper --- app/helpers/footer_links_helper.rb | 11 +++++++++++ app/views/shared/_footer.html.haml | 2 -- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 app/helpers/footer_links_helper.rb diff --git a/app/helpers/footer_links_helper.rb b/app/helpers/footer_links_helper.rb new file mode 100644 index 0000000000..71e8683da0 --- /dev/null +++ b/app/helpers/footer_links_helper.rb @@ -0,0 +1,11 @@ +require 'web/cookies_consent' + +module FooterLinksHelper + def cookies_policy_link + link_to( t( '.footer_data_cookies_policy' ), '', 'cookies-policy-modal' => true, 'cookies-banner' => !Web::CookiesConsent.new(cookies, request.host).exists? && Spree::Config.cookies_consent_banner_toggle) + end + + def privacy_policy_link + link_to( t( '.footer_data_privacy_policy' ), Spree::Config.privacy_policy_url, target: '_blank' ) + end +end diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index 21a9525e0a..4142a8fbec 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -140,8 +140,6 @@ = t '.footer_legal_text_html', {content_license: link_to('CC BY-SA 3.0', 'https://creativecommons.org/licenses/by-sa/3.0/'), code_license: link_to('AGPL 3', 'https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)' )} %p.text-small %div - - cookies_policy_link = link_to( t( '.footer_data_cookies_policy' ), '', 'cookies-policy-modal' => true, 'cookies-banner' => !Web::CookiesConsent.new(cookies, request.host).exists? && Spree::Config.cookies_consent_banner_toggle) - - privacy_policy_link = link_to( t( '.footer_data_privacy_policy' ), Spree::Config.privacy_policy_url, :target => '_blank' ) - if Spree::Config.privacy_policy_url.present? = t '.footer_data_text_with_privacy_policy_html', {cookies_policy: cookies_policy_link.html_safe, privacy_policy: privacy_policy_link.html_safe } - else From c22ac0086b99e706890ad40f439e9b5bae0cf65e Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 17 Sep 2018 13:53:19 +0100 Subject: [PATCH 084/190] Moved cookies_policy_helper to Web engine and respective spec --- app/helpers/cookies_policy_helper.rb | 21 -------- .../app/helpers/web/cookies_policy_helper.rb | 23 ++++++++ .../helpers/cookies_policy_helper_spec.rb | 52 +++++++++++++++++++ spec/helpers/cookies_policy_helper_spec.rb | 51 ------------------ 4 files changed, 75 insertions(+), 72 deletions(-) delete mode 100644 app/helpers/cookies_policy_helper.rb create mode 100644 engines/web/app/helpers/web/cookies_policy_helper.rb create mode 100644 engines/web/spec/helpers/cookies_policy_helper_spec.rb delete mode 100644 spec/helpers/cookies_policy_helper_spec.rb diff --git a/app/helpers/cookies_policy_helper.rb b/app/helpers/cookies_policy_helper.rb deleted file mode 100644 index 5bcec92383..0000000000 --- a/app/helpers/cookies_policy_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -module CookiesPolicyHelper - def render_cookie_entry(cookie_name, cookie_desc, cookie_domain = nil) - render partial: 'cookies_policy_entry', - locals: { cookie_name: cookie_name, - cookie_desc: cookie_desc, - cookie_domain: cookie_domain } - end - - def matomo_iframe_src - "#{Spree::Config.matomo_url}"\ - "/index.php?module=CoreAdminHome&action=optOut"\ - "&language=#{locale_language}"\ - "&backgroundColor=&fontColor=222222&fontSize=16px&fontFamily=%22Roboto%22%2C%20Arial%2C%20sans-serif" - end - - # removes country from locale if needed - # for example, both locales en and en_GB return language en - def locale_language - I18n.locale[0..1] - end -end diff --git a/engines/web/app/helpers/web/cookies_policy_helper.rb b/engines/web/app/helpers/web/cookies_policy_helper.rb new file mode 100644 index 0000000000..0fb86f7793 --- /dev/null +++ b/engines/web/app/helpers/web/cookies_policy_helper.rb @@ -0,0 +1,23 @@ +module Web + module CookiesPolicyHelper + def render_cookie_entry(cookie_name, cookie_desc, cookie_domain = nil) + render partial: 'cookies_policy_entry', + locals: { cookie_name: cookie_name, + cookie_desc: cookie_desc, + cookie_domain: cookie_domain } + end + + def matomo_iframe_src + "#{Spree::Config.matomo_url}"\ + "/index.php?module=CoreAdminome&action=optOut"\ + "&language=#{locale_language}"\ + "&backgroundColor=&fontColor=222222&fontSize=16px&fontFamily=%22Roboto%22%2C%20Arial%2C%20sans-serif" + end + + # removes country from locale if needed + # for example, both locales en and en_GB return language en + def locale_language + I18n.locale[0..1] + end + end +end diff --git a/engines/web/spec/helpers/cookies_policy_helper_spec.rb b/engines/web/spec/helpers/cookies_policy_helper_spec.rb new file mode 100644 index 0000000000..b4d7f7fdf3 --- /dev/null +++ b/engines/web/spec/helpers/cookies_policy_helper_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +module Web + describe CookiesPolicyHelper, type: :helper do + # keeps global state unchanged + around do |example| + original_locale = I18n.locale + original_matomo_url = Spree::Config.matomo_url + example.run + Spree::Config.matomo_url = original_matomo_url + I18n.locale = original_locale + end + + describe "matomo optout iframe src" do + describe "when matomo url is set" do + before do + Spree::Config.matomo_url = "http://matomo.org/" + end + + scenario "includes the matomo URL" do + expect(helper.matomo_iframe_src).to include Spree::Config.matomo_url + end + + scenario "is not equal to the matomo URL" do + expect(helper.matomo_iframe_src).to_not eq Spree::Config.matomo_url + end + end + + scenario "is not nil, when matomo url is nil" do + Spree::Config.matomo_url = nil + expect(helper.matomo_iframe_src).to_not eq nil + end + end + + describe "language from locale" do + scenario "when locale is the language" do + I18n.locale = "en" + expect(helper.locale_language).to eq "en" + end + + scenario "is empty when locale is empty" do + I18n.locale = "" + expect(helper.locale_language).to be_empty + end + + scenario "is only the language, when locale includes country" do + I18n.locale = "en_GB" + expect(helper.locale_language).to eq "en" + end + end + end +end diff --git a/spec/helpers/cookies_policy_helper_spec.rb b/spec/helpers/cookies_policy_helper_spec.rb deleted file mode 100644 index b09f578a32..0000000000 --- a/spec/helpers/cookies_policy_helper_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'spec_helper' - -describe CookiesPolicyHelper, type: :helper do - - # keeps global state unchanged - around do |example| - original_locale = I18n.locale - original_matomo_url = Spree::Config.matomo_url - example.run - Spree::Config.matomo_url = original_matomo_url - I18n.locale = original_locale - end - - describe "matomo optout iframe src" do - describe "when matomo url is set" do - before do - Spree::Config.matomo_url = "http://matomo.org/" - end - - scenario "includes the matomo URL" do - expect(helper.matomo_iframe_src).to include Spree::Config.matomo_url - end - - scenario "is not equal to the matomo URL" do - expect(helper.matomo_iframe_src).to_not eq Spree::Config.matomo_url - end - end - - scenario "is not nil, when matomo url is nil" do - Spree::Config.matomo_url = nil - expect(helper.matomo_iframe_src).to_not eq nil - end - end - - describe "language from locale" do - scenario "when locale is the language" do - I18n.locale = "en" - expect(helper.locale_language).to eq "en" - end - - scenario "is empty when locale is empty" do - I18n.locale = "" - expect(helper.locale_language).to be_empty - end - - scenario "is only the language, when locale includes country" do - I18n.locale = "en_GB" - expect(helper.locale_language).to eq "en" - end - end -end From ccf7d9148756e72fdd729950139dd056d604f782 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 17 Sep 2018 19:16:27 +0000 Subject: [PATCH 085/190] Bump oj from 2.1.2 to 3.6.10 Bumps [oj](https://github.com/ohler55/oj) from 2.1.2 to 3.6.10. - [Release notes](https://github.com/ohler55/oj/releases) - [Changelog](https://github.com/ohler55/oj/blob/master/CHANGELOG.md) - [Commits](https://github.com/ohler55/oj/commits/v3.6.10) Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 25844f45e9..a9223bd916 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -538,7 +538,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - oj (2.1.2) + oj (3.6.10) orm_adapter (0.5.0) paper_trail (3.0.9) activerecord (>= 3.0, < 5.0) From 13f73b45129bb61f41a2ffc28681531984199c1c Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 13 Sep 2018 11:25:41 +0200 Subject: [PATCH 086/190] Test :restart_checkout state machine event --- spec/models/spree/order/checkout_spec.rb | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 spec/models/spree/order/checkout_spec.rb diff --git a/spec/models/spree/order/checkout_spec.rb b/spec/models/spree/order/checkout_spec.rb new file mode 100644 index 0000000000..a78e7bb3a5 --- /dev/null +++ b/spec/models/spree/order/checkout_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Spree::Order do + describe 'event :restart_checkout' do + context 'when the order is not complete' do + let(:order) { create(:order) } + + before { allow(order).to receive(:completed?) { false } } + + it 'does transition to cart state' do + expect(order.state).to eq('cart') + end + end + + context 'when the order is complete' do + let(:order) { create(:order) } + + before { allow(order).to receive(:completed?) { true } } + + it 'raises' do + expect { order.restart_checkout! } + .to raise_error( + StateMachine::InvalidTransition, + /Cannot transition state via :restart_checkout/ + ) + end + end + end +end From 99cdeca0b188431a641e88860e1823be31001743 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 17 Sep 2018 16:23:06 +0200 Subject: [PATCH 087/190] Remove useless RSpec context block --- spec/models/spree/order_updater_spec.rb | 132 ++++++++++++------------ 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/spec/models/spree/order_updater_spec.rb b/spec/models/spree/order_updater_spec.rb index 50ab49a8c7..79bf384d49 100644 --- a/spec/models/spree/order_updater_spec.rb +++ b/spec/models/spree/order_updater_spec.rb @@ -4,87 +4,85 @@ describe Spree::OrderUpdater do # Copied pretty much verbatim from Spree 2.4. Remove this file once we get there, # assuming the unchanged 2.4 logic still works for us. # Only changes are stubs of :empty? instead of :size - context "updating payment state" do - let(:order) { Spree::Order.new } - let(:updater) { order.updater } + let(:order) { Spree::Order.new } + let(:updater) { order.updater } - it "is failed if no valid payments" do - order.stub_chain(:payments, :valid, :empty?).and_return(true) + it "is failed if no valid payments" do + order.stub_chain(:payments, :valid, :empty?).and_return(true) - updater.update_payment_state - order.payment_state.should == 'failed' + updater.update_payment_state + order.payment_state.should == 'failed' + end + + context "payment total is greater than order total" do + it "is credit_owed" do + order.payment_total = 2 + order.total = 1 + + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'credit_owed' + end + end + + context "order total is greater than payment total" do + it "is credit_owed" do + order.payment_total = 1 + order.total = 2 + + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'balance_due' + end + end + + context "order total equals payment total" do + it "is paid" do + order.payment_total = 30 + order.total = 30 + + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'paid' + end + end + + context "order is canceled" do + before do + order.state = 'canceled' end - context "payment total is greater than order total" do - it "is credit_owed" do - order.payment_total = 2 - order.total = 1 + context "and is still unpaid" do + it "is void" do + order.payment_total = 0 + order.total = 30 + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'void' + end + end + context "and is paid" do + it "is credit_owed" do + order.payment_total = 30 + order.total = 30 + order.stub_chain(:payments, :valid, :empty?).and_return(false) + order.stub_chain(:payments, :completed, :empty?).and_return(false) expect { updater.update_payment_state }.to change { order.payment_state }.to 'credit_owed' end end - context "order total is greater than payment total" do - it "is credit_owed" do - order.payment_total = 1 - order.total = 2 - - expect { - updater.update_payment_state - }.to change { order.payment_state }.to 'balance_due' - end - end - - context "order total equals payment total" do - it "is paid" do - order.payment_total = 30 + context "and payment is refunded" do + it "is void" do + order.payment_total = 0 order.total = 30 - + order.stub_chain(:payments, :valid, :empty?).and_return(false) + order.stub_chain(:payments, :completed, :empty?).and_return(false) expect { updater.update_payment_state - }.to change { order.payment_state }.to 'paid' - end - end - - context "order is canceled" do - before do - order.state = 'canceled' - end - - context "and is still unpaid" do - it "is void" do - order.payment_total = 0 - order.total = 30 - expect { - updater.update_payment_state - }.to change { order.payment_state }.to 'void' - end - end - - context "and is paid" do - it "is credit_owed" do - order.payment_total = 30 - order.total = 30 - order.stub_chain(:payments, :valid, :empty?).and_return(false) - order.stub_chain(:payments, :completed, :empty?).and_return(false) - expect { - updater.update_payment_state - }.to change { order.payment_state }.to 'credit_owed' - end - end - - context "and payment is refunded" do - it "is void" do - order.payment_total = 0 - order.total = 30 - order.stub_chain(:payments, :valid, :empty?).and_return(false) - order.stub_chain(:payments, :completed, :empty?).and_return(false) - expect { - updater.update_payment_state - }.to change { order.payment_state }.to 'void' - end + }.to change { order.payment_state }.to 'void' end end end From 1fdc57890135cdc3ea751e34bde564a51e6f4d02 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 17 Sep 2018 16:48:01 +0200 Subject: [PATCH 088/190] Increase readability (a bit) of OrderUpdater --- app/models/spree/order_updater_decorator.rb | 17 +++++++++-- spec/models/spree/order_updater_spec.rb | 31 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/app/models/spree/order_updater_decorator.rb b/app/models/spree/order_updater_decorator.rb index ab1d4eaf26..6ef5d2033c 100644 --- a/app/models/spree/order_updater_decorator.rb +++ b/app/models/spree/order_updater_decorator.rb @@ -8,7 +8,8 @@ module Spree # https://github.com/spree/spree/commit/38b8456183d11fc1e00e395e7c9154c76ef65b85 # https://github.com/spree/spree/commit/7b264acff7824f5b3dc6651c106631d8f30b147a def update_payment_state - last_state = order.payment_state + last_payment_state = order.payment_state + if payments.present? && payments.valid.empty? order.payment_state = 'failed' elsif order.state == 'canceled' && order.payment_total.zero? @@ -26,16 +27,26 @@ module Spree # order.payment_state = 'credit_owed' if order.outstanding_balance < 0 # order.payment_state = 'paid' if !order.outstanding_balance? end - order.state_changed('payment') if last_state != order.payment_state + + track_payment_state_change(last_payment_state) order.payment_state end private + def track_payment_state_change(last_payment_state) + return unless last_payment_state != order.payment_state + order.state_changed('payment') + end + # Taken from order.outstanding_balance in Spree 2.4 # See: https://github.com/spree/spree/commit/7b264acff7824f5b3dc6651c106631d8f30b147a def canceled_and_paid_for? - order.canceled? && order.payments.present? && !order.payments.completed.empty? + order.canceled? && paid? + end + + def paid? + order.payments.present? && !order.payments.completed.empty? end end end diff --git a/spec/models/spree/order_updater_spec.rb b/spec/models/spree/order_updater_spec.rb index 79bf384d49..957d7c596e 100644 --- a/spec/models/spree/order_updater_spec.rb +++ b/spec/models/spree/order_updater_spec.rb @@ -86,4 +86,35 @@ describe Spree::OrderUpdater do end end end + + context 'when the set payment_state does not match the last payment_state' do + before { order.payment_state = 'previous_to_paid' } + + context 'and the order is being updated' do + before { allow(order).to receive(:persisted?) { true } } + + it 'creates a new state_change for the order' do + expect { updater.update_payment_state } + .to change { order.state_changes.size }.by(1) + end + end + + context 'and the order is being created' do + before { allow(order).to receive(:persisted?) { false } } + + it 'creates a new state_change for the order' do + expect { updater.update_payment_state } + .not_to change { order.state_changes.size } + end + end + end + + context 'when the set payment_state matches the last payment_state' do + before { order.payment_state = 'paid' } + + it 'does not create any state_change' do + expect { updater.update_payment_state } + .not_to change { order.state_changes.size } + end + end end From 2a0e0eed734cedfed428b5c253cc542cb3c56a98 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 18 Sep 2018 10:06:12 +0200 Subject: [PATCH 089/190] Move state_machine's additions inside class_eval And also cover them with tests. --- app/models/spree/order_decorator.rb | 18 +++++++------ spec/models/spree/order_spec.rb | 42 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index bc8142694b..5baf95fa1c 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -45,6 +45,12 @@ Spree::Order.class_eval do remove_transition :from => :delivery, :to => :confirm end + state_machine.after_transition to: :payment, do: :charge_shipping_and_payment_fees! + + state_machine.event :restart_checkout do + transition to: :cart, unless: :completed? + end + # -- Scopes scope :managed_by, lambda { |user| if user.has_spree_role?('admin') @@ -414,17 +420,13 @@ Spree::Order.class_eval do adjustment.state = state end - # object_params sets the payment amount to the order total, but it does this before - # the shipping method is set. This results in the customer not being charged for their - # order's shipping. To fix this, we refresh the payment amount here. + # object_params sets the payment amount to the order total, but it does this + # before the shipping method is set. This results in the customer not being + # charged for their order's shipping. To fix this, we refresh the payment + # amount here. def charge_shipping_and_payment_fees! update_totals return unless payments.any? payments.first.update_attribute :amount, total end end - -Spree::Order.state_machine.after_transition to: :payment, do: :charge_shipping_and_payment_fees! -Spree::Order.state_machine.event :restart_checkout do - transition :to => :cart, unless: :completed? -end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 345cbb661f..e07fa41d54 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -858,4 +858,46 @@ describe Spree::Order do end end end + + describe '#restart_checkout!' do + let(:order) { build(:order) } + + context 'when the order is complete' do + before { order.completed_at = Time.zone.now } + + it 'raises' do + expect { order.restart_checkout! } + .to raise_error(StateMachine::InvalidTransition) + end + end + + context 'when the is not complete' do + before { order.completed_at = nil } + + it 'transitions to :cart state' do + order.restart_checkout! + expect(order.state).to eq('cart') + end + end + end + + describe '#charge_shipping_and_payment_fees!' do + let(:order) do + build(:order, shipping_method: build(:shipping_method)) + end + + context 'after transitioning to payment' do + before do + order.state = 'delivery' # payment's previous state + + allow(order).to receive(:payment_required?) { true } + allow(order).to receive(:charge_shipping_and_payment_fees!) + end + + it 'calls charge_shipping_and_payment_fees!' do + order.next + expect(order).to have_received(:charge_shipping_and_payment_fees!) + end + end + end end From 258347bc63f548479869908ce1a431d010c5f722 Mon Sep 17 00:00:00 2001 From: Transifex-Openfoodnetwork Date: Wed, 19 Sep 2018 01:45:12 +1000 Subject: [PATCH 090/190] Updating translations for config/locales/fr.yml --- config/locales/fr.yml | 90 ++++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 3b49c7ffbc..e7980fab89 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -64,6 +64,9 @@ fr: user_passwords: spree_user: updated_not_active: "Votre mot de passe a bien été réinitialisé, mais votre email n'a pas encore été confirmé." + models: + order_cycle: + cloned_order_cycle_name: "Copie de %{order_cycle}" enterprise_mailer: confirmation_instructions: subject: "Confirmez l'adresse email pour %{enterprise}" @@ -71,9 +74,26 @@ fr: subject: "%{enterprise} est maintenant sur %{sitename}" invite_manager: subject: "%{enterprise} vous a invité comme manager" + order_mailer: + cancel_email: + dear_customer: "Cher Acheteur," + instructions: "Votre commande a été ANNULEE. Veuillez en prendre note et conserver pour preuve si besoin cette confirmation." + order_summary_canceled: "Résumé de la commande [ANNULEE]" + subject: "Annulation de Commande" + subtotal: "Sous-total : %{subtotal}" + total: "Total Commande : %{total}" producer_mailer: order_cycle: subject: "Rapport de cycle de vente pour %{producer}" + shipment_mailer: + shipped_email: + dear_customer: "Cher Acheteur," + instructions: "Votre commande a été expédiée" + shipment_summary: "Résumé de l'envoi" + subject: "Notification d'expédition" + thanks: "Merci pour votre commande." + track_information: "Informations de suivi : %{tracking}" + track_link: "Lien de suivi : %{url}" subscription_mailer: placement_summary_email: subject: Un résumé des dernières commandes récurrentes passées @@ -136,6 +156,7 @@ fr: free_trial: "Utilisation contre contribution libre" plus_tax: "plus TVA" min_bill_turnover_desc: "Quand le chiffre d'affaire dépasse %{mbt_amount}" + more: "Plus" say_no: "Non" say_yes: "Oui" then: puis @@ -382,6 +403,7 @@ fr: main_links: Liens du menu principal footer_and_external_links: Pied de page et Liens Externes your_content: Votre contenu + user_guide: Guide utilisateur enterprise_fees: index: title: Marges et Commissions @@ -767,6 +789,9 @@ fr: search_placeholder: Recherche par nom manage: Gérer manage_link: Paramètres + producer?: "Producteur ?" + package: "Pack" + status: "Statut" new_form: owner: Manager principal owner_tip: Le manager principal est l'individu qui porte la responsabilité principale de l'entreprise dans le contexte de l'utilisation d'Open Food France. @@ -778,6 +803,14 @@ fr: new: title: Nouvelle entreprise back_link: Revenir à la liste des entreprises + remove_logo: + remove: "Supprimer l'image" + removed_successfully: "Logo supprimé avec succès" + immediate_removal_warning: "Le logo sera supprimé juste après votre confirmation." + remove_promo_image: + remove: "Supprimer l'image" + removed_successfully: "Bannière supprimée avec succès" + immediate_removal_warning: "La bannière sera supprimée juste après votre confirmation." welcome: welcome_title: Bienvenue sur Open Food France ! welcome_text: 'Vous avez créé avec succès ' @@ -810,6 +843,9 @@ fr: save_reload: Sauvegarder et rafraichir la page coordinator_fees: add: Ajouter commission coordinateur + filters: + search_by_order_cycle_name: "Recherche par nom de Cycle de Vente..." + involving: "Concernant" form: incoming: Produits entrants (pouvant être mis en vente par les hubs) supplier: Fournisseur @@ -823,7 +859,6 @@ fr: delivery_details: Précisions retrait / livraison debug_info: Informations de débogage index: - involving: Concernant schedule: Rythme d'abonnement schedules: Rythmes d'abonnement adding_a_new_schedule: Ajouter un nouveau rythme d'abonnement @@ -1357,6 +1392,7 @@ fr: email_so_edit_true_html: "Vous pouvez effectuer des modifications jusqu'à la fermeture de la période de commande le %{orders_close_at}." email_so_edit_false_html: "Vous pouvez consulter les détails de cette commande à tout moment." email_so_contact_distributor_html: "Pour toute question contactez %{distributor} via %{email}." + email_so_contact_distributor_to_change_order_html: "Cette commande a été automatiquement créée en votre nom. Vous pouvez effectuer des modifications sur cette commande jusqu'à fermeture de la période de commande le %{orders_close_at} en contactant %{distributor} à %{email}." email_so_confirmation_intro_html: "Votre commande auprès de %{distributor} est maintenant confirmée" email_so_confirmation_explainer_html: "Cette commande a été automatiquement passée pour vous dans le cadre de votre abonnement, et a maintenant été confirmée." email_so_confirmation_details_html: "Voici les détails concernant cette commande auprès de %{distributor}:" @@ -1845,31 +1881,31 @@ fr: you_have_no_orders_yet: "Vous n'avez pas encore de commande" running_balance: "Solde courant" outstanding_balance: "Solde restant" - admin_entreprise_relationships: "Permissions Inter-entreprises" - admin_entreprise_relationships_everything: "Tout" - admin_entreprise_relationships_permits: "autorise" - admin_entreprise_relationships_seach_placeholder: "Chercher" - admin_entreprise_relationships_button_create: "Créer" - admin_entreprise_groups: "Groupes d'entreprises" - admin_entreprise_groups_name: "Nom" - admin_entreprise_groups_owner: "Manager principal" - admin_entreprise_groups_on_front_page: "Sur la page d'accueil?" - admin_entreprise_groups_entreprise: "Entreprises" - admin_entreprise_groups_data_powertip: "Le manager principal en charge de ce groupe." - admin_entreprise_groups_data_powertip_logo: "Il s'agit du logo du groupe" - admin_entreprise_groups_data_powertip_promo_image: "Cette image est affichée en haut du profil Groupe." - admin_entreprise_groups_contact: "Contact" - admin_entreprise_groups_contact_phone_placeholder: "ex: 06 13 24 35 46" - admin_entreprise_groups_contact_address1_placeholder: "ex: 24 rue de la croix verte" - admin_entreprise_groups_contact_city: "Ville" - admin_entreprise_groups_contact_city_placeholder: "ex: Bordeaux" - admin_entreprise_groups_contact_zipcode: "Code postal" - admin_entreprise_groups_contact_zipcode_placeholder: "ex: 14120" - admin_entreprise_groups_contact_state_id: "Département" - admin_entreprise_groups_contact_country_id: "Pays" - admin_entreprise_groups_web: "Liens web" - admin_entreprise_groups_web_twitter: "ex: @OpenFoodNet_fr" - admin_entreprise_groups_web_website_placeholder: "ex: www.monepicerieenligne.fr" + admin_enterprise_relationships: "Permissions Inter-entreprises" + admin_enterprise_relationships_everything: "Tout" + admin_enterprise_relationships_permits: "autorise" + admin_enterprise_relationships_seach_placeholder: "Rechercher" + admin_enterprise_relationships_button_create: "Créer" + admin_enterprise_groups: "Groupes d'entreprises" + admin_enterprise_groups_name: "Produit/Variante" + admin_enterprise_groups_owner: "Manager principal" + admin_enterprise_groups_on_front_page: "Sur la page d'accueil?" + admin_enterprise_groups_enterprise: "Entreprises" + admin_enterprise_groups_data_powertip: "Le manager principal en charge de ce groupe." + admin_enterprise_groups_data_powertip_logo: "Il s'agit du logo du groupe" + admin_enterprise_groups_data_powertip_promo_image: "Cette image est affichée en haut du profil Groupe." + admin_enterprise_groups_contact: "Contact" + admin_enterprise_groups_contact_phone_placeholder: "ex: 06 13 24 35 46" + admin_enterprise_groups_contact_address1_placeholder: "ex: 24 rue de la croix verte" + admin_enterprise_groups_contact_city: "Ville" + admin_enterprise_groups_contact_city_placeholder: "ex: Nantes" + admin_enterprise_groups_contact_zipcode: "Code postal" + admin_enterprise_groups_contact_zipcode_placeholder: "ex: 44000" + admin_enterprise_groups_contact_state_id: "Département" + admin_enterprise_groups_contact_country_id: "Pays" + admin_enterprise_groups_web: "Liens web" + admin_enterprise_groups_web_twitter: "ex: @OpenFoodNet_fr" + admin_enterprise_groups_web_website_placeholder: "ex: www.maferme.fr" admin_order_cycles: "Gérer les cycles de vente" open: "Ouvre" close: "Ferme" @@ -2005,7 +2041,7 @@ fr: report_payment_totals: 'Total des paiements' report_all: 'tous' report_order_cycle: "Cycle de vente:" - report_entreprises: "Entreprises:" + report_enterprises: "Entreprises:" report_users: "Managers:" report_tax_rates: TVA par taux report_tax_types: TVA par type de produit/service From f0d015be0d23a6104efd4b42ba125de3cc2dfad3 Mon Sep 17 00:00:00 2001 From: Transifex-Openfoodnetwork Date: Wed, 19 Sep 2018 08:34:42 +1000 Subject: [PATCH 091/190] Updating translations for config/locales/de_DE.yml --- config/locales/de_DE.yml | 255 +++++++++++++++++++++++++++------------ 1 file changed, 176 insertions(+), 79 deletions(-) diff --git a/config/locales/de_DE.yml b/config/locales/de_DE.yml index 938ee71f31..162b55d464 100644 --- a/config/locales/de_DE.yml +++ b/config/locales/de_DE.yml @@ -31,7 +31,7 @@ de_DE: attributes: subscription_line_items: at_least_one_product: "^ Bitte fügen Sie mindestens ein Produkt hinzu" - not_available: "^ %{name} ist im ausgewählten Plan nicht verfügbar" + not_available: "^ %{name} ist im ausgewählten Zeitplan nicht verfügbar" ends_at: after_begins_at: "Muss nach \"beginnt um\" sein " customer: @@ -64,6 +64,9 @@ de_DE: user_passwords: spree_user: updated_not_active: "Ihr Passwort wurde zurückgesetzt, aber ihre E-Mail muss noch bestätigt werden." + models: + order_cycle: + cloned_order_cycle_name: "Kopie von %{order_cycle}" enterprise_mailer: confirmation_instructions: subject: "Bitte bestätigen Sie die E-Mail-Adresse für %{enterprise}" @@ -71,9 +74,26 @@ de_DE: subject: "%{enterprise} ist jetzt auf %{sitename}" invite_manager: subject: "%{enterprise} hat Sie eingeladen, ein Manager zu sein" + order_mailer: + cancel_email: + dear_customer: "Sehr geehrter Kunde," + instructions: "Ihre Bestellung wurde storniert. Bitte bewahren Sie diese Stornierungsinformationen für Ihre Unterlagen auf." + order_summary_canceled: "Bestellübersicht [ABGESAGT]" + subject: "Stornierung der Bestellung" + subtotal: "Zwischensumme: %{subtotal}" + total: "Bestellsumme: %{total}" producer_mailer: order_cycle: subject: "Bestellungszyklusreport für %{producer}" + shipment_mailer: + shipped_email: + dear_customer: "Sehr geehrter Kunde," + instructions: "Ihre Bestellung wurde versandt" + shipment_summary: "Sendungszusammenfassung" + subject: "Versandbenachrichtigung" + thanks: "Danke für Ihr Geschäft." + track_information: "Tracking-Informationen: %{tracking}" + track_link: "Tracking-Link: %{url}" subscription_mailer: placement_summary_email: subject: Eine Zusammenfassung der kürzlich aufgegebenen Abonnementbestellungen @@ -136,6 +156,7 @@ de_DE: free_trial: "kostenloser Versuch" plus_tax: "zuzüglich Umsatzsteuer" min_bill_turnover_desc: "sobald der Umsatz %{mbt_amount} übersteigt" + more: "Mehr" say_no: "Nein" say_yes: "Ja" then: dann @@ -217,13 +238,13 @@ de_DE: image: Bild product: Produkt quantity: Menge - schedule: Plan + schedule: Zeitplan shipping: Versand shipping_method: Lieferart shop: Laden sku: Artikelnummer status_state: Status - tags: Markierwort + tags: '"tags"' variant: Variante weight: Gewicht volume: Volumen @@ -249,7 +270,7 @@ de_DE: viewing: "Zeigt: %{current_view_name}" description: Beschreibung whats_this: Was ist das? - tag_has_rules: "Vorhandene Regeln für dieses Tag: %{num}" + tag_has_rules: "Vorhandene Regeln für dieses \"tag\": %{num}" has_one_rule: "hat eine Regel" has_n_rules: "hat %{num} Regel(n)" unsaved_confirm_leave: "Es gibt ungespeicherte Änderungen auf dieser Seite. Möchten Sie ohne Speichern fortfahren?" @@ -377,10 +398,12 @@ de_DE: header: Header home_page: Homepage producer_signup_page: Hersteller-Anmeldeseite - hub_signup_page: Hub-Anmeldeseite + hub_signup_page: Hubregistrierseite group_signup_page: Gruppen-Anmeldeseite + main_links: Hauptmenü-Links footer_and_external_links: Fußzeile und externe Links your_content: Ihr Inhalt + user_guide: Benutzerhandbuch enterprise_fees: index: title: Unternehmensgebühren @@ -523,13 +546,13 @@ de_DE: new_products_alert_message: Es sind %{new_product_count} neue Produkte verfügbar, die in den Katalog aufgenommen werden können currently_empty: Ihr Katalog ist momentan leer. no_matching_products: Es wurden keine passenden Produkte im Katalog gefunden. - no_hidden_products: In diesem Katalog wurden keine Produkte verborgen - no_matching_hidden_products: Keine verborgenen Produkte entsprechen Ihren Suchkriterien + no_hidden_products: In diesem Katalog wurden keine Produkte ausgeblendet + no_matching_hidden_products: Keine ausgeblendeten Produkte entsprechen Ihren Suchkriterien no_new_products: Es sind keine neuen Produkte verfügbar, die zu diesem Katalog hinzugefügt werden können no_matching_new_products: Keine neuen Produkte entsprechen Ihren Suchkriterien inventory_powertip: Dies ist Ihr Produktkatalog. Um Produkte zu Ihrem Katalog hinzuzufügen, wählen Sie "Neue Produkte" aus dem Dropdown-Menü. - hidden_powertip: Diese Produkte wurden in Ihrem Katalog versteckt und können nicht in Ihrem Laden hinzugefügt werden. Sie können auf "Hinzufügen" klicken, um ein Produkt zu Ihrem Katalog hinzuzufügen. - new_powertip: Diese Produkte können Ihrem Katalog hinzugefügt werden. Klicken Sie auf "Hinzufügen", um ein Produkt zu Ihrem Katalog hinzuzufügen, oder auf "Ausblenden", um es aus der Ansicht auszublenden. Sie können Ihre Meinung später immer ändern! + hidden_powertip: Diese Produkte wurden in Ihrem Katalog ausgeblendet und können nicht zu Ihrem Laden hinzugefügt werden. Sie können auf "Hinzufügen" klicken, um ein Produkt zu Ihrem Katalog hinzuzufügen. + new_powertip: Diese Produkte können Ihrem Katalog hinzugefügt werden. Klicken Sie auf "Hinzufügen", um ein Produkt zu Ihrem Katalog hinzuzufügen, oder auf "Verbergen", um es aus der Ansicht auszublenden. Sie können Ihre Meinung später immer ändern! controls: back_to_my_inventory: Zurück zu meinem Katalog orders: @@ -702,11 +725,11 @@ de_DE: by_default: Standardmäßig no_rules_yet: Es gelten noch keine Standardregeln add_new_button: '+ Fügen Sie eine neue Standardregel hinzu' - no_tags_yet: Für dieses Unternehmen sind noch keine Tags vorhanden - no_rules_yet: Für dieses Tag gelten noch keine Regeln - for_customers_tagged: 'Für Kunden mit dem Tag:' + no_tags_yet: Für dieses Unternehmen sind noch keine "tags" vorhanden + no_rules_yet: Für dieses "tag" gelten noch keine Regeln + for_customers_tagged: 'Für Kunden mit dem "tag":' add_new_rule: '+ Neue Regel hinzufügen' - add_new_tag: '+ Neuen Tag hinzufügen' + add_new_tag: '+ Neues "tag" hinzufügen' users: email_confirmation_notice_html: "E-Mail-Bestätigung steht aus. Wir haben eine Bestätigungs-E-Mail an %{email} gesendet." resend: Erneut senden @@ -767,6 +790,9 @@ de_DE: search_placeholder: Suche nach Name manage: Verwalten manage_link: Einstellungen + producer?: "Erzeuger?" + package: "Paket" + status: "Status" new_form: owner: Inhaber owner_tip: Der Hauptnutzer, der für dieses Unternehmen verantwortlich ist. @@ -778,6 +804,14 @@ de_DE: new: title: Neues Unternehmen back_link: Zurück zur Unternehmensliste + remove_logo: + remove: "Entferne Bild" + removed_successfully: "Das Logo wurde erfolgreich entfernt" + immediate_removal_warning: "Das Logo wird sofort nach der Bestätigung entfernt." + remove_promo_image: + remove: "Entferne Bild" + removed_successfully: "Promo-Bild wurde erfolgreich entfernt" + immediate_removal_warning: "Das Werbebild wird sofort nach der Bestätigung entfernt." welcome: welcome_title: Willkommen im Open Food Network! welcome_text: 'Erfolgreich erstellt:' @@ -810,6 +844,11 @@ de_DE: save_reload: Speichern und neu laden coordinator_fees: add: Koordinatorgebühr hinzufügen + filters: + search_by_order_cycle_name: "Suche nach Bestellung Cycle name ..." + involving: "Involviert" + any_enterprise: "Jede Firma" + any_schedule: "Jeder Zeitplan" form: incoming: Eingehend supplier: Anbieter @@ -818,22 +857,21 @@ de_DE: outgoing: Ausgehend distributor: Verteiler products: Produkte - tags: Stichworte - add_a_tag: Füge einen Tag hinzu + tags: '"tags"' + add_a_tag: '"tag" hinzufügen' delivery_details: Abhol- / Lieferinformationen debug_info: Debug-Informationen index: - involving: Involviert schedule: Zeitplan - schedules: Termine + schedules: Zeitpläne adding_a_new_schedule: Hinzufügen eines neuen Zeitplans updating_a_schedule: Einen Zeitplan aktualisieren - new_schedule: Neue Terminplanung + new_schedule: Neuer Zeitplan create_schedule: Zeitplan erstellen update_schedule: Zeitplan aktualisieren delete_schedule: Zeitplan löschen created_schedule: Zeitplan erstellt - updated_schedule: Aktualisierter Zeitplan + updated_schedule: Zeitplan aktualisiert deleted_schedule: Zeitplan gelöscht schedule_name_placeholder: Zeitplanname name_required_error: Bitte geben Sie einen Namen für diesen Zeitplan ein @@ -856,7 +894,7 @@ de_DE: fees: Gebühren destroy_errors: orders_present: Dieser Bestellzyklus wurde von einem Kunden ausgewählt und kann nicht gelöscht werden. Um weitere Verwendung zu verhindern, können Sie ihn stattdessen schließen. - schedule_present: Dieser Bestellzyklus ist mit einem Zeitplan verknüpft und kann nicht gelöscht werden. Bitte heben Sie die Verknüpfung auf oder löschen Sie den Zeitplan zuerst. + schedule_present: Dieser Bestellzyklus ist mit einem Zeitplan verknüpft und kann nicht gelöscht werden. Bitte heben Sie zuerst die Verknüpfung auf oder löschen Sie den Zeitplan. bulk_update: no_data: Hm, etwas ist schief gelaufen. Keine Bestellzyklusdaten gefunden. date_warning: @@ -889,7 +927,7 @@ de_DE: email_confirmation: "E-Mail-Bestätigung steht aus. Wir haben eine Bestätigungs-E-Mail an %{email} gesendet." not_visible: "%{enterprise} ist nicht sichtbar und kann daher nicht auf der Karte oder in Suchen gefunden werden" reports: - hidden: VERSTECKT + hidden: Ausgeblendet unitsize: EINHEITSGRÖSSE total: SUMME total_items: GESAMTANZAHL @@ -1007,7 +1045,7 @@ de_DE: no_matching_subscriptions: Keine passenden Abonnements gefunden schedules: destroy: - associated_subscriptions_error: Dieser Zeitplan kann nicht gelöscht werden, da ihm Subskriptionen zugeordnet sind + associated_subscriptions_error: Dieser Zeitplan kann nicht gelöscht werden, da ihm Abonnements zugeordnet sind. controllers: enterprises: stripe_connect_cancelled: "Die Verbindung zu Stripe wurde abgebrochen" @@ -1015,6 +1053,11 @@ de_DE: stripe_connect_fail: Die Verbindung Ihres Stripe-Kontos ist fehlgeschlagen stripe_connect_settings: resource: Konfiguration für Stripe Connect + api: + enterprise_logo: + destroy_attachment_does_not_exist: "Logo existiert nicht" + enterprise_promo_image: + destroy_attachment_does_not_exist: "Promo-Bild existiert nicht" checkout: already_ordered: cart: "Warenkorb" @@ -1095,12 +1138,19 @@ de_DE: ticket_column_unit_price: "Stückpreis" ticket_column_total_price: "Gesamtpreis" menu_1_title: "Läden" + menu_1_url: "/ Geschäfte" menu_2_title: "Karte" + menu_2_url: "/Karte" menu_3_title: "Erzeuger" + menu_3_url: "/ Produzenten" menu_4_title: "Gruppen" + menu_4_url: "/ Gruppen" menu_5_title: "Über Uns" + menu_5_url: "http://www.openfoodnetwork.org/" menu_6_title: "Verbinde" + menu_6_url: "https://openfoodnetwork.org/au/connect/" menu_7_title: "Lerne" + menu_7_url: "https://openfoodnetwork.org/au/learn/" logo: "Logo (640x130)" logo_mobile: "Handylogo (75x26)" logo_mobile_svg: "Handylogo (SVG)" @@ -1116,6 +1166,7 @@ de_DE: footer_email: "E-Mail:" footer_links_md: "Links" footer_about_url: "Über URL" + user_guide_link: "Benutzeranleitung Link" name: Name first_name: Vorname last_name: Nachname @@ -1190,8 +1241,47 @@ de_DE: ie_warning_ie: Internet Explorer aktualisieren ie_warning_other: "Können Sie Ihren Browser nicht aktualisieren? Versuchen Sie Open Food Network auf Ihrem Smartphone :-)" legal: + cookies_policy: + header: "Wie wir Cookies verwenden" + desc_part_1: "Cookies sind sehr kleine Textdateien, die beim Besuch einiger Websites auf Ihrem Computer gespeichert werden." + desc_part_2: "In OFN respektieren wir Ihre Privatsphäre. Wir verwenden nur die Cookies, die notwendig sind, um Ihnen den Service des Online-Kaufs / -Verkaufs von Lebensmitteln zu bieten. Wir verkaufen keine Ihrer Daten. Wir könnten Ihnen in Zukunft vorschlagen, einige Ihrer Daten zu teilen, um neue Commons-Dienste zu entwickeln, die für das Ökosystem nützlich sein könnten (wie Logistikdienstleistungen für kurze Nahrungsmittelsysteme), aber wir sind noch nicht dort, und wir werden es nicht ohne Ihr tun Genehmigung :-)" + desc_part_3: "Wir verwenden Cookies hauptsächlich, um sich daran zu erinnern, wer Sie sind, wenn Sie sich bei dem Dienst anmelden oder sich die Artikel merken können, die Sie in Ihren Warenkorb legen, auch wenn Sie nicht eingeloggt sind \"Cookies akzeptieren\" nehmen wir an, dass Sie uns die Speicherung von Cookies erlauben, die für das Funktionieren der Website notwendig sind. Hier ist die Liste der von uns verwendeten Cookies!" + essential_cookies: "Essentielle Kekse" + essential_cookies_desc: "Die folgenden Cookies sind für den Betrieb unserer Website unbedingt erforderlich." + essential_cookies_note: "Die meisten Cookies enthalten nur einen eindeutigen Bezeichner, aber keine anderen Daten. Ihre E-Mail-Adresse und Ihr Kennwort sind zum Beispiel niemals darin enthalten und werden nie veröffentlicht." + cookie_domain: "Festlegen von:" + cookie_session_desc: "Wird verwendet, damit die Website sich zwischen den Seitenbesuchen an die Benutzer erinnern kann, z. B. an Artikel in Ihrem Einkaufswagen." + cookie_consent_desc: "Wird verwendet, um den Status der Benutzerzustimmung zum Speichern von Cookies beizubehalten" + cookie_remember_me_desc: "Wird verwendet, wenn der Benutzer die Website aufgefordert hat, sich an ihn zu erinnern. Dieser Cookie wird nach 12 Tagen automatisch gelöscht. Wenn Sie als Benutzer möchten, dass dieser Cookie gelöscht wird, müssen Sie sich nur abmelden. Wenn Sie nicht möchten, dass der Cookie auf Ihrem Computer installiert wird, sollten Sie das Kontrollkästchen \"An mich erinnern\" nicht aktivieren, wenn Sie sich anmelden." + cookie_openstreemap_desc: "Wird von unserem freundlichen Open-Source-Mapping-Anbieter (OpenStreetMap) verwendet, um sicherzustellen, dass während eines bestimmten Zeitraums nicht zu viele Anfragen eingehen, um den Missbrauch ihrer Dienste zu verhindern." + cookie_stripe_desc: "Daten gesammelt von unserem Zahlungsabwickler Stripe für die Betrugserkennung https://stripe.com/cookies-policy/legal. Nicht alle Geschäfte verwenden Stripe als Zahlungsmethode, aber es ist eine gute Vorgehensweise, sie auf alle Seiten anzuwenden. Stripe erstellt wahrscheinlich ein Bild davon, welche unserer Seiten normalerweise mit ihrer API interagieren und merkt, wenn etwas Ungewöhnliches passiert. Das Festlegen des Stripe-Cookies hat also eine breitere Funktion als die Bereitstellung einer Zahlungsmethode für einen Benutzer. Das Entfernen könnte die Sicherheit des Dienstes selbst beeinträchtigen. Sie können mehr über Stripe erfahren und dessen Datenschutzrichtlinie unter https://stripe.com/privacy lesen." + statistics_cookies: "Statistik-Cookies" + statistics_cookies_desc: "Die folgenden Punkte sind nicht unbedingt erforderlich, helfen Ihnen jedoch, die beste Benutzererfahrung zu bieten, indem wir das Benutzerverhalten analysieren, die am häufigsten verwendeten Funktionen identifizieren oder nicht verwenden, Probleme mit der Benutzerfreundlichkeit verstehen usw." + statistics_cookies_analytics_desc_html: "Zur Erfassung und Analyse von Daten zur Nutzung der Plattform verwenden wir Google Analytics, da es sich um den Standarddienst handelt, der mit Spree (der E-Commerce-Open-Source-Software, auf der wir aufgebaut haben) verbunden ist. Unsere Vision ist jedoch der Wechsel zu Matomo (ex Piwik, ein Open-Source-Analysetool, das der DSGVO entspricht und Ihre Privatsphäre schützt), sobald wir können." + statistics_cookies_matomo_desc_html: "Um Daten zur Nutzung der Plattform zu erfassen und zu analysieren, verwenden wir Matomo (ex Piwik), ein Open-Source-Analysetool, das der DSGVO-Richtlinie entspricht schützt Ihre Privatsphäre." + statistics_cookies_matomo_optout: "Möchten Sie Matomo Analytics deaktivieren? Wir sammeln keine persönlichen Daten und Matomo hilft uns, unseren Service zu verbessern, aber wir respektieren Ihre Wahl :-)" + cookie_analytics_utma_desc: "Wird zur Unterscheidung von Benutzern und Sitzungen verwendet. Der Cookie wird erstellt, wenn die JavaScript-Bibliothek ausgeführt wird und keine vorhandenen __utma-Cookies vorhanden sind. Der Cookie wird jedes Mal aktualisiert, wenn Daten an Google Analytics gesendet werden." + cookie_analytics_utmt_desc: "Wird zum Drosseln der Anforderungsrate verwendet." + cookie_analytics_utmb_desc: "Wird verwendet, um neue Sitzungen / Besuche zu bestimmen. Der Cookie wird erstellt, wenn die JavaScript-Bibliothek ausgeführt wird und keine vorhandenen __utmb-Cookies vorhanden sind. Der Cookie wird jedes Mal aktualisiert, wenn Daten an Google Analytics gesendet werden." + cookie_analytics_utmc_desc: "Wird nicht in ga.js verwendet Stellen Sie die Interoperabilität mit urchin.js ein. In der Vergangenheit wurde dieser Cookie in Verbindung mit dem __utmb-Cookie verwendet, um festzustellen, ob sich der Benutzer in einer neuen Sitzung / einem neuen Besuch befand." + cookie_analytics_utmz_desc: "Speichert die Zugriffsquelle oder Kampagne, die erläutert, wie der Nutzer Ihre Website erreicht hat. Der Cookie wird erstellt, wenn die JavaScript-Bibliothek ausgeführt wird, und wird jedes Mal aktualisiert, wenn Daten an Google Analytics gesendet werden." + cookie_matomo_basics_desc: "Matomo First Party Cookies zum Sammeln von Statistiken." + cookie_matomo_heatmap_desc: "Matomo Heatmap & Session Aufnahme-Cookie." + cookie_matomo_ignore_desc: "Cookie verwendet, um Benutzer von der Verfolgung auszuschließen." + disabling_cookies_header: "Warnung zum Deaktivieren von Cookies" + disabling_cookies_desc: "Als Nutzer können Sie Cookies von Open Food Network oder einer anderen Website immer zulassen, blockieren oder löschen, wann immer Sie die Einstellungskontrolle Ihres Browsers nutzen möchten. Jeder Browser hat einen anderen Benutzer. Hier sind die Links:" + disabling_cookies_firefox_link: "https://support.mozilla.org/en-US/kb/enable-and-disable-cookies-website-preferences" + disabling_cookies_chrome_link: "https://support.google.com/chrome/answer/95647" + disabling_cookies_ie_link: "https://support.microsoft.com/en-us/help/17442/windows-internet-explorer-delete-manage-cookies" + disabling_cookies_safari_link: "https://www.apple.com/legal/privacy/en-ww/cookies/" + disabling_cookies_note: "Beachten Sie jedoch, dass die Website nicht funktioniert, wenn Sie die essentiellen Cookies löschen oder ändern, die von Open Food Network verwendet werden. Sie können also nichts zum Warenkorb hinzufügen, auch nicht zur Kasse gehen." cookies_banner: + cookies_usage: "Diese Website verwendet Cookies, um Ihre Navigation reibungslos und sicher zu gestalten und um zu verstehen, wie Sie sie verwenden, um die von uns angebotenen Funktionen zu verbessern." + cookies_definition: "Cookies sind sehr kleine Textdateien, die beim Besuch einiger Websites auf Ihrem Computer gespeichert werden." + cookies_desc: "Wir verwenden nur die Cookies, die notwendig sind, um Ihnen den Service des Online-Kaufs / -Verkaufs von Lebensmitteln zu bieten. Wir verkaufen keine Ihrer Daten. Wir verwenden Cookies hauptsächlich, um sich daran zu erinnern, wer Sie sind, wenn Sie sich bei dem Dienst anmelden oder sich die Artikel merken können, die Sie in Ihren Warenkorb legen, auch wenn Sie nicht eingeloggt sind \"Cookies akzeptieren\" nehmen wir an, dass Sie uns die Speicherung von Cookies erlauben, die für das Funktionieren der Website notwendig sind." + cookies_policy_link_desc: "Wenn Sie mehr erfahren möchten, besuchen Sie unsere" cookies_policy_link: "Hinweise zu Cookies" + cookies_accept_button: "Cookies akzeptieren" home_shop: Jetzt einkaufen brandstory_headline: "Essen, ohne eigene Rechtspersönlichkeit." brandstory_intro: "Manchmal ist der beste Weg, das System zu reparieren, ein neues zu starten ..." @@ -1203,15 +1293,15 @@ de_DE: brandstory_part6: "Wir alle lieben das Essen. Jetzt können wir unser Nahrungssystem auch lieben." learn_body: "Erkunden Sie Modelle, Geschichten und Ressourcen, um Sie bei der Entwicklung Ihres Fair-Food-Geschäfts oder Ihrer Organisation zu unterstützen. Finden Sie Schulungen, Veranstaltungen und andere Möglichkeiten, um von Gleichgesinnten zu lernen." learn_cta: "Lass dich inspirieren" - connect_body: "Durchsuchen Sie unsere vollständigen Verzeichnisse von Herstellern, Hubs und Gruppen, um Fair-Food-Händler in Ihrer Nähe zu finden. Listen Sie Ihr Unternehmen oder Ihre Organisation auf dem OFN auf, damit Käufer Sie finden können. Treten Sie der Community bei, um Rat zu bekommen und Probleme gemeinsam zu lösen." + connect_body: "Durchsuchen Sie unsere vollständigen Verzeichnisse von Erzeugern, Hubs und Gruppen, um Fair-Food-Händler in Ihrer Nähe zu finden. Listen Sie Ihr Unternehmen oder Ihre Organisation auf dem OFN auf, damit Käufer Sie finden können. Treten Sie der Community bei, um Rat zu bekommen und Probleme gemeinsam zu lösen." connect_cta: "Erkunden" system_headline: "Einkaufen - so funktioniert es." system_step1: "1. Suche" system_step1_text: "Durchsuchen Sie unsere verschiedenen, unabhängigen Geschäfte nach saisonalem, lokalem Essen. Suche nach Gegend und Lebensmittelkategorie, oder ob Sie Lieferung oder Abholung bevorzugen." system_step2: "2. Laden" - system_step2_text: "Transformieren Sie Ihre Transaktionen mit erschwinglichen lokalen Lebensmitteln von verschiedenen Herstellern und Hubs. Kenne die Geschichten hinter deinem Essen und den Menschen, die es schaffen!" + system_step2_text: "Transformieren Sie Ihre Transaktionen mit erschwinglichen lokalen Lebensmitteln von verschiedenen Erzeugern und Hubs. Erfahren Sie die Geschichten hinter Ihrem Essen und den Menschen, die es erzeugen." system_step3: "3. Abholung / Lieferung" - system_step3_text: "Erhalten Sie eine Lieferung oder besuchen Sie Ihren Hersteller oder Hub für eine persönlichere Verbindung mit Ihrem Essen. Lebensmitteleinkauf so vielfältig wie die Natur es beabsichtigt hat." + system_step3_text: "Erhalten Sie eine Lieferung oder besuchen Sie Ihren Hersteller oder Ihr Hub für eine persönlichere Verbindung mit Ihrem Essen. Lebensmitteleinkauf so vielfältig wie die Natur es beabsichtigt hat." cta_headline: "Einkaufen, das die Welt verbessert." cta_label: "Ich bin bereit" stats_headline: "Wir schaffen ein neues Ernährungssystem." @@ -1222,7 +1312,7 @@ de_DE: checkout_title: Kasse checkout_now: Zur Kasse checkout_order_ready: Bestellung bereit für - checkout_hide: Verstecke + checkout_hide: Ausblenden checkout_expand: Erweitern checkout_headline: "Ok, jetzt zur Kasse?" checkout_as_guest: "Als Gast zur Kasse" @@ -1312,6 +1402,7 @@ de_DE: email_so_edit_true_html: "Sie können Änderungen vornehmen , bis Bestellungen auf %{orders_close_at} schließen." email_so_edit_false_html: "Sie können jederzeit Details zu dieser Bestellung anzeigen ." email_so_contact_distributor_html: "Wenn Sie Fragen haben, können Sie sich mit %{distributor} über %{email} in Verbindung setzen." + email_so_contact_distributor_to_change_order_html: "Diese Bestellung wurde automatisch für Sie erstellt. Sie können Änderungen vornehmen, bis Bestellungen auf %{orders_close_at} geschlossen werden, indem Sie %{distributor} über %{email} kontaktieren." email_so_confirmation_intro_html: "Ihre Bestellung mit %{distributor} ist jetzt bestätigt" email_so_confirmation_explainer_html: "Diese Bestellung wurde automatisch für Sie aufgegeben und ist nun abgeschlossen." email_so_confirmation_details_html: "Hier finden Sie alles, was Sie über Ihre Bestellung wissen müssen: %{distributor} :" @@ -1422,7 +1513,7 @@ de_DE: groups_signup_motivation2: Deshalb sind wir jeden Tag aufgestanden. Wir sind eine globale Non-Profit-Organisation, die auf Open-Source-Code basiert. Wir spielen fair. Sie können uns immer vertrauen. groups_signup_motivation3: Wir wissen, dass Sie große Ideen haben und wir helfen wollen. Wir teilen unser Wissen, Netzwerke und Ressourcen. Wir wissen, dass Isolation keine Veränderung verursacht, also werden wir mit Ihnen zusammenarbeiten. groups_signup_motivation4: Wir treffen dich, wo du bist. - groups_signup_motivation5: Sie könnten eine Allianz von Food-Hubs, Produzenten oder Distributoren, einer Industrieorganisation oder einer lokalen Regierung sein. + groups_signup_motivation5: Sie könnten eine Allianz von Hubs, Produzenten oder Vertailern sein, eine Industrieorganisation oder eine Lokalbehörde. groups_signup_motivation6: Was auch immer Ihre Rolle in Ihrer lokalen Nahrungsmittelbewegung ist, wir sind bereit zu helfen. Wie auch immer Sie sich fragen, wie Open Food Network in Ihrem Teil der Welt aussehen würde oder wird, lassen Sie uns das Gespräch beginnen. groups_signup_motivation7: Wir machen Nahrungsmittelbewegungen sinnvoller. groups_signup_motivation8: Sie müssen Ihre Netzwerke aktivieren und aktivieren, wir bieten eine Plattform für Konversation und Aktion. Sie brauchen echtes Engagement. Wir helfen, alle Akteure, alle Beteiligten, alle Sektoren zu erreichen. @@ -1433,8 +1524,8 @@ de_DE: groups_signup_contact_text: "Kontaktieren Sie uns, um herauszufinden, was OFN für Sie tun kann:" groups_signup_detail: "Hier ist das Detail." login_invalid: "Ungültige E-Mail-Adresse oder ungültiges Passwort" - modal_hubs: "Essen Hubs" - modal_hubs_abstract: Unsere Food-Hubs sind der Kontaktpunkt zwischen Ihnen und den Menschen, die Ihr Essen zubereiten! + modal_hubs: "Lebensmittel-Hubs" + modal_hubs_abstract: Unsere Hubs sind der Kontaktpunkt zwischen Ihnen und den Menschen, die Ihre Lebensmittel herstellen! modal_hubs_content1: Sie können nach einem geeigneten Hub nach Standort oder Namen suchen. Einige Hubs haben mehrere Punkte, an denen Sie Ihre Einkäufe abholen können, und einige bieten auch Lieferoptionen. Jeder Food-Hub ist eine Verkaufsstelle mit eigenständigem Geschäftsbetrieb und Logistik - so sind Unterschiede zwischen den Hubs zu erwarten. modal_hubs_content2: Sie können nicht bei mehr als einem Hub gleichzeitig einkaufen. modal_groups: "Gruppen / Regionen" @@ -1481,7 +1572,7 @@ de_DE: sell_headline: "Steigen Sie in das Open Food Netzwerk ein!" sell_motivation: "Stellen Sie Ihr schönes Essen vor." sell_producers: "Produzenten" - sell_hubs: "Naben" + sell_hubs: "Hubs" sell_groups: "Gruppen" sell_producers_detail: "Richten Sie in wenigen Minuten ein Profil für Ihr Unternehmen auf dem OFN ein. Sie können Ihr Profil jederzeit auf einen Online-Shop aktualisieren und Ihre Produkte direkt an Kunden verkaufen." sell_hubs_detail: "Richten Sie im OFN ein Profil für Ihr Lebensmittelunternehmen oder Ihre Organisation ein. Sie können Ihr Profil jederzeit auf einen Multi-Producer-Shop upgraden." @@ -1493,8 +1584,8 @@ de_DE: shops_title: Läden shops_headline: Einkaufen, verwandelt. shops_text: Nahrung wächst in Zyklen, Bauern ernten in Zyklen und wir bestellen Nahrung in Zyklen. Wenn Sie feststellen, dass ein Bestellzyklus geschlossen ist, schauen Sie bald wieder vorbei. - shops_signup_title: Melde dich als Hub an - shops_signup_headline: Food-Hubs, unbegrenzt. + shops_signup_title: Als Hub registrieren + shops_signup_headline: Lebensmittel-Hubs, unbegrenzt. shops_signup_motivation: Was auch immer Ihr Modell ist, wir unterstützen Sie. Wie auch immer Sie sich ändern, wir sind bei Ihnen. Wir sind gemeinnützig, unabhängig und Open-Source. Wir sind die Software-Partner, von denen Sie schon immer geträumt haben. shops_signup_action: Jetzt beitreten shops_signup_pricing: Unternehmenskonten @@ -1514,7 +1605,7 @@ de_DE: orders_form_admin: Admin & Handhabung orders_form_total: Total orders_oc_expired_headline: Bestellungen wurden für diesen Bestellzyklus geschlossen - orders_oc_expired_text: "Entschuldigung, Bestellungen für diesen Bestellzyklus haben %{time} geschlossen! Bitte kontaktieren Sie direkt Ihren Hub, um zu sehen, ob sie verspätete Bestellungen annehmen können." + orders_oc_expired_text: "Entschuldigung, Bestellungen für diesen Bestellzyklus wurden vor %{time} geschlossen! Bitte kontaktieren Sie Ihr Hub direkt, um zu sehen, ob sie verspätete Bestellungen annehmen können." orders_oc_expired_text_others_html: "Entschuldigung, Bestellungen für diesen Bestellzyklus haben %{time} geschlossen! Wenden Sie sich direkt an Ihren Hub, um zu sehen, ob er verspätete Bestellungen annehmen kann %{link} ." orders_oc_expired_text_link: "oder sehen Sie die anderen Bestellzyklen an diesem Hub" orders_oc_expired_email: "Email:" @@ -1573,6 +1664,7 @@ de_DE: error_number: "muss nummer sein" error_email: "muss eine E-Mail-Adresse sein" error_not_found_in_database: "%{name} nicht in Datenbank gefunden" + error_not_primary_producer: "%{name} ist nicht als Producer aktiviert" error_no_permission_for_enterprise: "\"%{name}\": Sie sind nicht berechtigt, Produkte für dieses Unternehmen zu verwalten" item_handling_fees: "Artikel Bearbeitungsgebühren (in den Gesamtsummen enthalten)" january: "Januar" @@ -1799,31 +1891,31 @@ de_DE: you_have_no_orders_yet: "Du hast noch keine Bestellungen" running_balance: "Laufendes Gleichgewicht" outstanding_balance: "Offener Betrag" - admin_entreprise_relationships: "Unternehmensberechtigungen" - admin_entreprise_relationships_everything: "Alles" - admin_entreprise_relationships_permits: "Genehmigungen" - admin_entreprise_relationships_seach_placeholder: "Suche" - admin_entreprise_relationships_button_create: "Neu" - admin_entreprise_groups: "Unternehmensgruppen" - admin_entreprise_groups_name: "Name" - admin_entreprise_groups_owner: "Inhaber" - admin_entreprise_groups_on_front_page: "Auf der ersten Seite?" - admin_entreprise_groups_entreprise: "Unternehmen" - admin_entreprise_groups_data_powertip: "Der primäre Benutzer, der für diese Gruppe verantwortlich ist." - admin_entreprise_groups_data_powertip_logo: "Dies ist das Logo für die Gruppe" - admin_entreprise_groups_data_powertip_promo_image: "Dieses Bild wird oben im Gruppenprofil angezeigt" - admin_entreprise_groups_contact: "Kontakt" - admin_entreprise_groups_contact_phone_placeholder: "z.B. 98 7654 3210" - admin_entreprise_groups_contact_address1_placeholder: "z.B. Gartenweg 12" - admin_entreprise_groups_contact_city: "Vorort" - admin_entreprise_groups_contact_city_placeholder: "z.B. Northcote" - admin_entreprise_groups_contact_zipcode: "Postleitzahl" - admin_entreprise_groups_contact_zipcode_placeholder: "z.B. 3070" - admin_entreprise_groups_contact_state_id: "Bundesland" - admin_entreprise_groups_contact_country_id: "Land" - admin_entreprise_groups_web: "Webressourcen" - admin_entreprise_groups_web_twitter: "z.B. @the_prof" - admin_entreprise_groups_web_website_placeholder: "z.B. www.truffles.com" + admin_enterprise_relationships: "Unternehmensberechtigungen" + admin_enterprise_relationships_everything: "Alles" + admin_enterprise_relationships_permits: "Genehmigungen" + admin_enterprise_relationships_seach_placeholder: "Suche" + admin_enterprise_relationships_button_create: "Neu" + admin_enterprise_groups: "Unternehmensgruppen" + admin_enterprise_groups_name: "Name" + admin_enterprise_groups_owner: "Inhaber" + admin_enterprise_groups_on_front_page: "Auf der ersten Seite?" + admin_enterprise_groups_enterprise: "Unternehmen" + admin_enterprise_groups_data_powertip: "Der primäre Benutzer, der für diese Gruppe verantwortlich ist." + admin_enterprise_groups_data_powertip_logo: "Dies ist das Logo für die Gruppe" + admin_enterprise_groups_data_powertip_promo_image: "Dieses Bild wird oben im Gruppenprofil angezeigt" + admin_enterprise_groups_contact: "Kontakt" + admin_enterprise_groups_contact_phone_placeholder: "z.B. 98 7654 3210" + admin_enterprise_groups_contact_address1_placeholder: "z.B. Gartenstrasse 123" + admin_enterprise_groups_contact_city: "Vorort" + admin_enterprise_groups_contact_city_placeholder: "z.B. Nordwestheim" + admin_enterprise_groups_contact_zipcode: "Postleitzahl" + admin_enterprise_groups_contact_zipcode_placeholder: "z.B. 30701" + admin_enterprise_groups_contact_state_id: "Status" + admin_enterprise_groups_contact_country_id: "Land" + admin_enterprise_groups_web: "Webressourcen" + admin_enterprise_groups_web_twitter: "z.B. @the_prof" + admin_enterprise_groups_web_website_placeholder: "z.B. www.truffles.com" admin_order_cycles: "Admin-Bestellzyklen" open: "Öffnen" close: "Abschließen" @@ -1893,10 +1985,10 @@ de_DE: spree_admin_unit_value: Einheitswert spree_admin_unit_description: Einheit Beschreibung spree_admin_variant_unit: Varianteneinheit - spree_admin_variant_unit_scale: Variationseinheitsskala + spree_admin_variant_unit_scale: Einheitsmaß der Variante spree_admin_supplier: Anbieter spree_admin_product_category: Produktkategorie - spree_admin_variant_unit_name: Name der Varianteinheit + spree_admin_variant_unit_name: Einheit der Variante change_package: "Paket ändern" spree_admin_single_enterprise_hint: "Tipp: Um es Ihnen zu ermöglichen, Sie zu finden, aktivieren Sie Ihre Sichtbarkeit unter" spree_admin_eg_pickup_from_school: "z.B. \"Abholung von der Grundschule\"" @@ -1959,7 +2051,7 @@ de_DE: report_payment_totals: 'Zahlungssummen' report_all: 'alle' report_order_cycle: "Bestellzyklus:" - report_entreprises: "Unternehmen:" + report_enterprises: "Unternehmen:" report_users: "Benutzer:" report_tax_rates: Steuersätze report_tax_types: Steuerarten @@ -2002,7 +2094,7 @@ de_DE: report_header_order_number: Bestellnummer report_header_date: Datum report_header_confirmation_date: Bestätigungsdatum - report_header_tags: Stichworte + report_header_tags: '"tags"' report_header_items: Artikel report_header_items_total: "Artikel insgesamt %{currency_symbol}" report_header_taxable_items_total: "Steuerpflichtige Posten insgesamt (%{currency_symbol})" @@ -2132,7 +2224,7 @@ de_DE: payment_methods: "Zahlungsarten" payment_method_fee: "Transaktionsgebühr" inventory_settings: "Katalogeinstellungen" - tag_rules: "Tag-Regeln" + tag_rules: "\"tag\"-Regeln" shop_preferences: "Ladeneinstellungen" enterprise_fee_whole_order: Ganze Bestellung enterprise_fee_by: "%{type} Gebühr von %{role} %{enterprise_name}" @@ -2213,22 +2305,21 @@ de_DE: invite: "Einladen" invite_title: "Laden Sie einen nicht registrierten Benutzer ein" tag_rule_help: - title: Tag-Regeln + title: '"tag"-Regeln' overview: Überblick overview_text: > - Mit Tag-Regeln können Sie beschreiben, welche Elemente für welche Kunden - sichtbar sind oder nicht. Artikel können Versandarten, Zahlungsarten, - Produkte und Bestellzyklen sein. + Mit "tag"-Regeln können Sie beschreiben, welche Elemente für welche + Kunden sichtbar sind. Elemente können Versandarten, Zahlungsarten, Produkte + und Bestellzyklen sein. by_default_rules: "\"Standardmäßig ...\" Regeln" by_default_rules_text: > - Mit Standardregeln können Sie Elemente ausblenden, sodass sie standardmäßig + Mit Standardregeln können Sie Elemente verbergen, sodass sie standardmäßig nicht sichtbar sind. Dieses Verhalten kann dann durch nicht standardmäßige - Regeln für Kunden mit bestimmten Tags überschrieben werden. - customer_tagged_rules: "'Kunden getagged ...' Regeln" + Regeln für Kunden mit bestimmten "tags" überschrieben werden. + customer_tagged_rules: "'Kunden mit \"tag\" ...' Regeln" customer_tagged_rules_text: > - Durch das Erstellen von Regeln für ein bestimmtes Kundentag können Sie - das Standardverhalten (ob Elemente anzeigen oder ausblenden) für Kunden - mit dem angegebenen Tag überschreiben. + Durch das Erstellen von Regeln für ein bestimmtes "tag" können Sie die + Standardregeln für bestimmte Kunden überschreiben. panels: save: SPEICHERN saved: GERETTET @@ -2342,7 +2433,7 @@ de_DE: only_n_remainging: "Jetzt hat nur noch %{num} übrig." variant_overrides: inventory_products: "Katalogprodukte" - hidden_products: "Versteckte Produkte" + hidden_products: "Ausgeblendete Produkte" new_products: "Neue Produkte" reset_stock_levels: Zurücksetzen der Bestandswerte auf Standardwerte changes_to: Änderungen an @@ -2355,10 +2446,10 @@ de_DE: changing_on_hand_stock: Änderung der Lagerbestände ... stock_reset: Aktien werden auf Standardwerte zurückgesetzt. tag_rules: - show_hide_variants: 'Produktvarianten im Laden anzeigen oder verbergen' - show_hide_shipping: 'Lieferarten in der Kasse anzeigen oder verbergen' - show_hide_payment: 'Zahlungsarten an der Kasse anzeigen oder verbergen' - show_hide_order_cycles: 'Bestellzyklen in meinem Laden anzeigen order verbergen' + show_hide_variants: 'Produktvarianten im Laden anzeigen?' + show_hide_shipping: 'Lieferarten an der Kasse anzeigen?' + show_hide_payment: 'Zahlungsarten an der Kasse anzeigen?' + show_hide_order_cycles: 'Bestellzyklen in meinem Laden anzeigen?' visible: SICHTBAR not_visible: UNSICHTBAR services: @@ -2376,7 +2467,9 @@ de_DE: Dadurch wird der Lagerbestand für alle Produkte auf Null gesetzt Unternehmen, die in der hochgeladenen Datei nicht vorhanden sind. order_cycles: + create_failure: "Fehler beim Erstellen des Bestellzyklus" update_success: 'Ihr Bestellzyklus wurde aktualisiert.' + update_failure: "Fehler beim Aktualisieren des Bestellzyklus" no_distributors: In diesem Bestellzyklus gibt es keine Distributoren. Dieser Bestellzyklus ist für Kunden erst sichtbar, wenn Sie einen hinzufügen. Möchten Sie diesen Bestellzyklus weiterhin speichern? enterprises: producer: "Erzeuger" @@ -2397,6 +2490,10 @@ de_DE: my_account: "Mein Konto" date: "Datum" time: "Zeit" + layouts: + admin: + header: + store: Geschäft admin: orders: invoice: @@ -2455,7 +2552,7 @@ de_DE: av_on: "Verfüg. am" import_date: "Importdatum" products_variant: - variant_has_n_overrides: "Diese Variante hat %{n} override (s)" + variant_has_n_overrides: "Diese Variante hat %{n} Überschreibungen" new_variant: "Neue Variante" product_name: Produktname primary_taxon_form: From 93d273f94a7293a5910097e10261a5621202912b Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Wed, 12 Sep 2018 18:00:21 +0100 Subject: [PATCH 092/190] Convert orders index table to use angular ng-repeat --- .../controllers/orders_controller.js.coffee | 10 +- .../admin/resources/services/orders.js.coffee | 6 +- app/serializers/api/admin/order_serializer.rb | 75 ++++++++++++- .../spree/admin/orders/_capture.html.haml | 7 -- app/views/spree/admin/orders/index.html.haml | 105 ++++++++++-------- config/locales/en.yml | 2 + 6 files changed, 143 insertions(+), 62 deletions(-) delete mode 100644 app/views/spree/admin/orders/_capture.html.haml diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index afe3e87d2e..abbf84a06f 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.orders").controller "ordersCtrl", ($scope, $compile, $attrs, Orders) -> +angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $compile, $attrs, Orders) -> $scope.$compile = $compile $scope.shops = shops $scope.orderCycles = orderCycles @@ -6,9 +6,15 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, $compile, $attr $scope.distributor_id = parseInt($attrs.ofnDistributorId) $scope.order_cycle_id = parseInt($attrs.ofnOrderCycleId) + $scope.RequestMonitor = RequestMonitor $scope.orders = Orders.all + $scope.per_page = 15 + $scope.page = 1 - Orders.index(per_page: 15) + Orders.index( + per_page: $scope.per_page, + page: $scope.page + ) $scope.validOrderCycle = (oc) -> $scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id) diff --git a/app/assets/javascripts/admin/resources/services/orders.js.coffee b/app/assets/javascripts/admin/resources/services/orders.js.coffee index da3f409149..22377d3a8e 100644 --- a/app/assets/javascripts/admin/resources/services/orders.js.coffee +++ b/app/assets/javascripts/admin/resources/services/orders.js.coffee @@ -1,12 +1,14 @@ -angular.module("admin.resources").factory 'Orders', ($q, OrderResource) -> +angular.module("admin.resources").factory 'Orders', ($q, OrderResource, RequestMonitor) -> new class Orders byID: {} pristineByID: {} index: (params={}, callback=null) -> - OrderResource.index params, (data) => + request = OrderResource.index params, (data) => @load(data) (callback || angular.noop)(data) + RequestMonitor.load(request.$promise) + request load: (orders) -> for order in orders diff --git a/app/serializers/api/admin/order_serializer.rb b/app/serializers/api/admin/order_serializer.rb index a010636307..065025d22f 100644 --- a/app/serializers/api/admin/order_serializer.rb +++ b/app/serializers/api/admin/order_serializer.rb @@ -1,5 +1,8 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer - attributes :id, :number, :full_name, :email, :phone, :completed_at, :payment_total + attributes :id, :number, :full_name, :email, :phone, :completed_at, :display_total + attributes :show_path, :edit_path, :state, :payment_state, :shipment_state + attributes :payments_path, :shipments_path, :ship_path, :ready_to_ship, :created_at + attributes :distributor_name, :special_instructions, :pending_payments, :capture_path has_one :distributor, serializer: Api::Admin::IdSerializer has_one :order_cycle, serializer: Api::Admin::IdSerializer @@ -8,6 +11,48 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer object.billing_address.nil? ? "" : ( object.billing_address.full_name || "" ) end + def distributor_name + object.distributor.andand.name + end + + def show_path + return '' unless object.id + Spree::Core::Engine.routes_url_helpers.admin_order_path(object) + end + + def edit_path + return '' unless object.id + Spree::Core::Engine.routes_url_helpers.edit_admin_order_path(object) + end + + def payments_path + return '' unless object.payment_state + Spree::Core::Engine.routes_url_helpers.admin_order_payments_path(object) + end + + def shipments_path + return '' unless object.shipment_state + Spree::Core::Engine.routes_url_helpers.admin_order_shipments_path(object) + end + + def ship_path + Spree::Core::Engine.routes_url_helpers.fire_admin_order_path(object, e: 'ship') + end + + def capture_path + return '' unless ready_for_payment? + return unless payment_to_capture + Spree::Core::Engine.routes_url_helpers.fire_admin_order_payment_path(object, payment_to_capture.id, e: 'capture') + end + + def ready_to_ship + object.ready_to_ship? + end + + def display_total + object.display_total.to_html + end + def email object.email || "" end @@ -16,7 +61,33 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer object.billing_address.nil? ? "a" : ( object.billing_address.phone || "" ) end + def created_at + object.created_at.blank? ? "" : I18n.l(object.created_at, format: '%B %d, %Y') + end + def completed_at - object.completed_at.blank? ? "" : object.completed_at.strftime("%F %T") + object.completed_at.blank? ? "" : I18n.l(object.completed_at, format: '%B %d, %Y') + end + + def pending_payments + return if object.payments.blank? + payment = object.payments.select{ |p| p if p.state == 'checkout' }.first + return unless can_be_captured? payment + + payment.id + end + + private + + def ready_for_payment? + object.payment_required? && object.payments.present? + end + + def payment_to_capture + object.payments.select{ |p| p if p.state == 'checkout' }.first + end + + def can_be_captured?(payment) + payment && payment.actions.include?('capture') end end diff --git a/app/views/spree/admin/orders/_capture.html.haml b/app/views/spree/admin/orders/_capture.html.haml deleted file mode 100644 index b1267badbb..0000000000 --- a/app/views/spree/admin/orders/_capture.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- # Get the payment in 'checkout' state if any, and show capture button -- if order.payments.present? - - payment = order.payments.select{|p| p if p.state == 'checkout'}.first - - if !payment.nil? - - payment.actions.grep(/^capture$/).each do |action| - - # copied from backend/app/views/spree/admin/payments/_list.html.erb - = link_to_with_icon "icon-#{action}", t('admin.orders.index.capture'), fire_admin_order_payment_path(order, payment, :e => action), :method => :put, :no_text => true, :data => {:action => action} diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 68e1763d99..44f5bd3432 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -8,7 +8,7 @@ = render partial: 'spree/admin/shared/order_sub_menu' - content_for :app_wrapper_attrs do - = "ng-app='admin.orders'" + = "ng-app='admin.orders' ng-controller='ordersCtrl'" - content_for :table_filter_title do = t(:search) @@ -65,53 +65,60 @@ %div{"data-hook" => "admin_orders_index_search_buttons"} = button t(:filter_results), 'icon-search' -- unless @orders.empty? - %table#listing_orders.index.responsive{"data-hook" => "", width: "100%", "ng-controller" => "ordersCtrl"} - %colgroup - %col{style: "width: 10%"} - %thead - %tr{"data-hook" => "admin_orders_index_headers"} - %th - = t(:products_distributor) - - if @show_only_completed - %th= sort_link @search, :completed_at, t(:completed_at, :scope => 'activerecord.attributes.spree/order') - - else - %th= sort_link @search, :created_at, t(:created_at, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :number, t(:number, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :state, t(:state, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :payment_state, t(:payment_state, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :shipment_state, t(:shipment_state, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :email, t(:email, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :total, t(:total, :scope => 'activerecord.attributes.spree/order') - %th.actions{"data-hook" => "admin_orders_index_header_actions"} - %tbody - - @orders.each do |order| - %tr{class: "state-#{order.state.downcase} #{cycle('odd', 'even')}", "data-hook" => "admin_orders_index_rows"} - %td.align-center - = order.distributor.andand.name - %td.align-center= l (@show_only_completed ? order.completed_at : order.created_at).to_date - %td - = link_to order.number, admin_order_path(order) - - if order.special_instructions.present? - %br - %span{class: "icon-warning-sign", "ofn-with-tip" => order.special_instructions} - notes - %td.align-center - %span{class: "state #{order.state.downcase}"}= t("order_state.#{order.state.downcase}") - %td.align-center - %span{class: "state #{order.payment_state}"}= link_to t("payment_states.#{order.payment_state}"), admin_order_payments_path(order) if order.payment_state - %td.align-center - %span{class: "state #{order.shipment_state}"}= link_to t("shipment_states.#{order.shipment_state}"), admin_order_shipments_path(order) if order.shipment_state - %td= mail_to order.email - %td.align-center= order.display_total.to_html - %td.actions{"data-hook" => "admin_orders_index_row_actions"} - = link_to_edit_url edit_admin_order_path(order), :title => "admin_edit_#{dom_id(order)}", :no_text => true - - if order.ready_to_ship? - - # copied from backend/app/views/spree/admin/payments/_list.html.erb - = link_to_with_icon "icon-road", t('admin.orders.index.ship'), fire_admin_order_url(order, :e => 'ship'), :method => :put, :no_text => true, :data => {:action => 'ship', :confirm => t(:are_you_sure)} - = render partial: 'spree/admin/orders/capture', locals: {order: order} -- else - .no-objects-found - = t(:no_orders_found) +%table#listing_orders.index.responsive{"data-hook" => "", width: "100%", 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } + %colgroup + %col{style: "width: 10%"} + %thead + %tr{"data-hook" => "admin_orders_index_headers"} + %th + = t(:products_distributor) + - if @show_only_completed + %th= sort_link @search, :completed_at, t(:completed_at, :scope => 'activerecord.attributes.spree/order') + - else + %th= sort_link @search, :created_at, t(:created_at, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :number, t(:number, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :state, t(:state, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :payment_state, t(:payment_state, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :shipment_state, t(:shipment_state, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :email, t(:email, :scope => 'activerecord.attributes.spree/order') + %th= sort_link @search, :total, t(:total, :scope => 'activerecord.attributes.spree/order') + %th.actions{"data-hook" => "admin_orders_index_header_actions"} + %tbody + %tr{ng: {repeat: 'order in orders track by $index', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'", "data-hook" => "admin_orders_index_rows"} + %td.align-center + {{::order.distributor_name}} + %td.align-center + = @show_only_completed ? '{{::order.completed_at}}' : '{{::order.created_at}}' + %td + %a{'ng-href' => '{{::order.show_path}}'} + {{order.number}} + %div{'ng-if' => 'order.special_instructions'} + %br + %span.icon-warning-sign{'ofn-with-tip' => "{{::order.special_instructions}}"} + = t(:note) + %td.align-center + %span.state{'ng-class' => 'order.state'} + {{'order_state.' + order.state | t}} + %td.align-center + %span.state{'ng-class' => 'order.payment_state', 'ng-if' => 'order.payment_state'} + %a{'ng-href' => '{{::order.payments_path}}' } + {{'payment_states.' + order.payment_state | t}} + %td.align-center + %span.state{'ng-class' => 'order.shipment_state', 'ng-if' => 'order.shipment_state'} + %a{'ng-href' => '{{::order.shipments_path}}' } + {{'shipment_states.' + order.shipment_state | t}} + %td + = mail_to "{{::order.email}}" + %td.align-center + %span{'ng-bind-html' => '::order.display_total'} + %td.actions{"data-hook" => "admin_orders_index_row_actions"} + %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{::order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t(:edit)} + %div{'ng-if' => 'order.ready_to_ship'} + %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{::order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t(:ship)} + %div{'ng-if' => 'order.pending_payments'} + %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t(:capture)} + +.no-objects-found{'ng-show' => "!RequestMonitor.loading && orders.length == 0"} + = t(:no_orders_found) = paginate @orders diff --git a/config/locales/en.yml b/config/locales/en.yml index b9dabc2129..8250e549d3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -616,6 +616,8 @@ en: index: capture: "Capture" ship: "Ship" + edit: "Edit" + note: "Note" invoice_email_sent: 'Invoice email has been sent' order_email_resent: 'Order email has been resent' bulk_management: From 524f9af148cc431ae80dc8a834ffabf06b627a04 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Wed, 19 Sep 2018 11:38:19 +0100 Subject: [PATCH 093/190] Drop unused db column line_items.shipping_method_name --- ..._shipping_method_name_from_spree_line_items.rb | 9 +++++++++ db/schema.rb | 15 +++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 db/migrate/20180919102548_remove_shipping_method_name_from_spree_line_items.rb diff --git a/db/migrate/20180919102548_remove_shipping_method_name_from_spree_line_items.rb b/db/migrate/20180919102548_remove_shipping_method_name_from_spree_line_items.rb new file mode 100644 index 0000000000..eaae250a18 --- /dev/null +++ b/db/migrate/20180919102548_remove_shipping_method_name_from_spree_line_items.rb @@ -0,0 +1,9 @@ +class RemoveShippingMethodNameFromSpreeLineItems < ActiveRecord::Migration + def up + remove_column :spree_line_items, :shipping_method_name + end + + def down + add_column :spree_line_items, :shipping_method_name, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 13c0c125b8..91ef541a32 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 => 20180812214434) do +ActiveRecord::Schema.define(:version => 20180919102548) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -527,15 +527,14 @@ ActiveRecord::Schema.define(:version => 20180812214434) do create_table "spree_line_items", :force => true do |t| t.integer "order_id" t.integer "variant_id" - t.integer "quantity", :null => false - t.decimal "price", :precision => 8, :scale => 2, :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.integer "quantity", :null => false + t.decimal "price", :precision => 8, :scale => 2, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "max_quantity" t.string "currency" - t.decimal "distribution_fee", :precision => 10, :scale => 2 - t.string "shipping_method_name" - t.decimal "final_weight_volume", :precision => 10, :scale => 2 + t.decimal "distribution_fee", :precision => 10, :scale => 2 + t.decimal "final_weight_volume", :precision => 10, :scale => 2 end add_index "spree_line_items", ["order_id"], :name => "index_line_items_on_order_id" From 3b9d9db16b058e57b8eb6ac8e59a8515f2374d0f Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Thu, 13 Sep 2018 11:00:05 +0100 Subject: [PATCH 094/190] Add pagination --- .../controllers/orders_controller.js.coffee | 20 +++++++++++++------ .../resources/order_resource.js.coffee | 1 - .../admin/resources/services/orders.js.coffee | 6 ++++-- .../admin/components/pagination.scss | 20 +++++++++++++++++++ .../admin/orders_controller_decorator.rb | 9 ++++++++- .../orders/_angular_pagination.html.haml | 17 ++++++++++++++++ app/views/spree/admin/orders/index.html.haml | 7 ++++--- config/locales/en.yml | 4 ++++ 8 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 app/assets/stylesheets/admin/components/pagination.scss create mode 100644 app/views/spree/admin/orders/_angular_pagination.html.haml diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index abbf84a06f..7243c0f178 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -8,13 +8,16 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $scope.RequestMonitor = RequestMonitor $scope.orders = Orders.all - $scope.per_page = 15 - $scope.page = 1 + $scope.pagination = Orders.pagination - Orders.index( - per_page: $scope.per_page, - page: $scope.page - ) + $scope.initialise = -> + $scope.fetchResults() + + $scope.fetchResults = -> + Orders.index({ + per_page: $scope.per_page || 15, + page: $scope.page || 1 + }) $scope.validOrderCycle = (oc) -> $scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id) @@ -29,6 +32,11 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $scope.distributionChosen = -> $scope.distributor_id && $scope.order_cycle_id + $scope.changePage = (newPage) -> + $scope.page = newPage + Orders.resetData() + $scope.fetchResults() + for oc in $scope.orderCycles oc.name_and_status = "#{oc.name} (#{oc.status})" diff --git a/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee b/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee index 317b384485..d5679c629e 100644 --- a/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee +++ b/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee @@ -2,7 +2,6 @@ angular.module("admin.resources").factory 'OrderResource', ($resource) -> $resource('/admin/orders/:id/:action.json', {}, { 'index': method: 'GET' - isArray: true 'update': method: 'PUT' }) diff --git a/app/assets/javascripts/admin/resources/services/orders.js.coffee b/app/assets/javascripts/admin/resources/services/orders.js.coffee index 22377d3a8e..c4ec6b4a80 100644 --- a/app/assets/javascripts/admin/resources/services/orders.js.coffee +++ b/app/assets/javascripts/admin/resources/services/orders.js.coffee @@ -2,6 +2,7 @@ angular.module("admin.resources").factory 'Orders', ($q, OrderResource, RequestM new class Orders byID: {} pristineByID: {} + pagination: {} index: (params={}, callback=null) -> request = OrderResource.index params, (data) => @@ -10,8 +11,9 @@ angular.module("admin.resources").factory 'Orders', ($q, OrderResource, RequestM RequestMonitor.load(request.$promise) request - load: (orders) -> - for order in orders + load: (data) -> + angular.extend(@pagination, data.pagination) + for order in data.orders @byID[order.id] = order @pristineByID[order.id] = angular.copy(order) diff --git a/app/assets/stylesheets/admin/components/pagination.scss b/app/assets/stylesheets/admin/components/pagination.scss new file mode 100644 index 0000000000..fd3705a6fd --- /dev/null +++ b/app/assets/stylesheets/admin/components/pagination.scss @@ -0,0 +1,20 @@ +@import "admin/variables"; + +.pagination { + text-align: center; + margin: 2em 0 1em; + + button { + margin: 0 0.35em; + + &.active { + background-color: darken($spree-blue, 15%); + cursor: default; + } + + &.disabled { + background-color: #ccc; + cursor: default; + } + } +} diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index 43d1ce16b8..8c0c914d9e 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -61,7 +61,14 @@ Spree::Admin::OrdersController.class_eval do respond_with(@orders) do |format| format.html format.json do - render_as_json @orders + render json: { + orders: ActiveModel::ArraySerializer.new(@orders, each_serializer: Api::Admin::OrderSerializer), + pagination: { + results: @orders.total_count, + pages: @orders.num_pages, + page: params[:page].to_i + } + } end end end diff --git a/app/views/spree/admin/orders/_angular_pagination.html.haml b/app/views/spree/admin/orders/_angular_pagination.html.haml new file mode 100644 index 0000000000..53a28af885 --- /dev/null +++ b/app/views/spree/admin/orders/_angular_pagination.html.haml @@ -0,0 +1,17 @@ +.pagination + %button{'ng-click' => 'changePage(1)', 'ng-class' => "{'disabled': pagination.page == 1}", 'ng-disabled' => "pagination.page == 1"} + = "«".html_safe + = t(:first) + %button{'ng-click' => 'changePage((pagination.page)-1)', 'ng-class' => "{'disabled': pagination.page == 1}", 'ng-disabled' => "pagination.page == 1"} + = t(:previous) + %span{'ng-show' => 'pagination.page > 3'} + = "…".html_safe + %button{'ng-repeat' => 'i in [].constructor(pagination.pages) track by $index', 'ng-show' =>'($index+1 > pagination.page-3 || (pagination.page > pagination.pages-2 && $index+1 > pagination.pages-5)) && ($index+1 < pagination.page+3 || (pagination.page < 3 && $index+1 < 6))', 'ng-class' => "{'active': pagination.page == $index+1}", 'ng-click' => 'changePage($index+1)', 'ng-disabled' => "pagination.page == $index+1"} + {{$index+1}} + %span{'ng-show' => 'pagination.page < pagination.pages-2'} + = "…".html_safe + %button{'ng-click' => 'changePage((pagination.page)+1)', 'ng-class' => "{'disabled': pagination.page == pagination.pages}", 'ng-disabled' => "pagination.page == pagination.pages"} + = t(:next) + %button{'ng-click' => 'changePage(pagination.pages)', 'ng-class' => "{'disabled': pagination.page == pagination.pages}", 'ng-disabled' => "pagination.page == pagination.pages"} + = t(:last) + = "»".html_safe diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 44f5bd3432..f7f4d90a11 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -65,7 +65,7 @@ %div{"data-hook" => "admin_orders_index_search_buttons"} = button t(:filter_results), 'icon-search' -%table#listing_orders.index.responsive{"data-hook" => "", width: "100%", 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } +%table#listing_orders.index.responsive{"data-hook" => "", width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } %colgroup %col{style: "width: 10%"} %thead @@ -118,7 +118,8 @@ %div{'ng-if' => 'order.pending_payments'} %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t(:capture)} +%div{'ng-show' => "!RequestMonitor.loading && orders.length > 0" } + = render partial: 'angular_pagination' + .no-objects-found{'ng-show' => "!RequestMonitor.loading && orders.length == 0"} = t(:no_orders_found) - -= paginate @orders diff --git a/config/locales/en.yml b/config/locales/en.yml index 8250e549d3..16a85986b1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -618,6 +618,10 @@ en: ship: "Ship" edit: "Edit" note: "Note" + first: "First" + last: "Last" + previous: "Previous" + next: "Next" invoice_email_sent: 'Invoice email has been sent' order_email_resent: 'Order email has been resent' bulk_management: From 2112f296e42e6e955f770d6164e60c8e7eab816a Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Thu, 13 Sep 2018 12:33:10 +0100 Subject: [PATCH 095/190] Angularise filters --- .../controllers/orders_controller.js.coffee | 24 +++++++-- .../admin/resources/services/orders.js.coffee | 8 +++ .../spree/admin/orders/_filters.html.haml | 51 ++++++++++++++++++ app/views/spree/admin/orders/index.html.haml | 53 +------------------ 4 files changed, 80 insertions(+), 56 deletions(-) create mode 100644 app/views/spree/admin/orders/_filters.html.haml diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 7243c0f178..21a5d42b91 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -7,16 +7,31 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $scope.order_cycle_id = parseInt($attrs.ofnOrderCycleId) $scope.RequestMonitor = RequestMonitor - $scope.orders = Orders.all $scope.pagination = Orders.pagination + $scope.orders = Orders.all $scope.initialise = -> + $scope.q = { + completed_at_not_null: true + } $scope.fetchResults() - $scope.fetchResults = -> + $scope.fetchResults = (page=1) -> Orders.index({ + 'q[created_at_lt]': $scope['q']['created_at_lt'], + 'q[created_at_gt]': $scope['q']['created_at_gt'], + 'q[state_eq]': $scope['q']['state_eq'], + 'q[number_cont]': $scope['q']['number_cont'], + 'q[email_cont]': $scope['q']['email_cont'], + 'q[bill_address_firstname_start]': $scope['q']['bill_address_firstname_start'], + 'q[bill_address_lastname_start]': $scope['q']['bill_address_lastname_start'], + 'q[completed_at_not_null]': $scope['q']['completed_at_not_null'], + 'q[inventory_units_shipment_id_null]': $scope['q']['inventory_units_shipment_id_null'], + 'q[distributor_id_in]': $scope['q']['distributor_id_in'], + 'q[order_cycle_id_in]': $scope['q']['order_cycle_id_in'], + 'q[order_cycle_id_in]': $scope['q']['order_cycle_id_in'], per_page: $scope.per_page || 15, - page: $scope.page || 1 + page: page }) $scope.validOrderCycle = (oc) -> @@ -34,8 +49,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $scope.changePage = (newPage) -> $scope.page = newPage - Orders.resetData() - $scope.fetchResults() + $scope.fetchResults(newPage) for oc in $scope.orderCycles oc.name_and_status = "#{oc.name} (#{oc.status})" diff --git a/app/assets/javascripts/admin/resources/services/orders.js.coffee b/app/assets/javascripts/admin/resources/services/orders.js.coffee index c4ec6b4a80..332dc44796 100644 --- a/app/assets/javascripts/admin/resources/services/orders.js.coffee +++ b/app/assets/javascripts/admin/resources/services/orders.js.coffee @@ -1,5 +1,6 @@ angular.module("admin.resources").factory 'Orders', ($q, OrderResource, RequestMonitor) -> new class Orders + all: [] byID: {} pristineByID: {} pagination: {} @@ -13,10 +14,17 @@ angular.module("admin.resources").factory 'Orders', ($q, OrderResource, RequestM load: (data) -> angular.extend(@pagination, data.pagination) + @clearData() for order in data.orders + @all.push order @byID[order.id] = order @pristineByID[order.id] = angular.copy(order) + clearData: -> + @all.length = 0 + @byID = {} + @pristineByID = {} + save: (order) -> deferred = $q.defer() order.$update({id: order.number}) diff --git a/app/views/spree/admin/orders/_filters.html.haml b/app/views/spree/admin/orders/_filters.html.haml new file mode 100644 index 0000000000..5ebea2a4cd --- /dev/null +++ b/app/views/spree/admin/orders/_filters.html.haml @@ -0,0 +1,51 @@ +%div{"data-hook" => "admin_orders_index_search"} + = search_form_for [:admin, @search], html: { name: "orders_form", "ng-submit" => "fetchResults()"} do |f| + .field-block.alpha.four.columns + .date-range-filter.field + = label_tag nil, t(:date_range) + .date-range-fields + = f.text_field :created_at_gt, class: 'datepicker', datepicker: 'q.created_at_gt', 'ng-model' => 'q.created_at_gt', :value => params[:q][:created_at_gt], :placeholder => t(:start) + %span.range-divider + %i.icon-arrow-right + = f.text_field :created_at_lt, class: 'datepicker', datepicker: 'q.created_at_lt', 'ng-model' => 'q.created_at_lt', :value => params[:q][:created_at_lt], :placeholder => t(:stop) + .field + = label_tag nil, t(:status) + = f.select :state_eq, Spree::Order.state_machines[:state].states.collect {|s| [t("order_state.#{s.name}"), s.value]}, {:include_blank => true}, :class => 'select2', 'ng-model' => 'q.state_eq' + .four.columns + .field + = label_tag nil, t(:order_number) + = f.text_field :number_cont, 'ng-model' => 'q.number_cont' + .field + = label_tag nil, t(:email) + = f.email_field :email_cont, 'ng-model' => 'q.email_cont' + .four.columns + .field + = label_tag nil, t(:first_name_begins_with) + = f.text_field :bill_address_firstname_start, :size => 25, 'ng-model' => 'q.bill_address_firstname_start' + .field + = label_tag nil, t(:last_name_begins_with) + = f.text_field :bill_address_lastname_start, :size => 25, 'ng-model' => 'q.bill_address_lastname_start' + .omega.four.columns + .field.checkbox + %label + = f.check_box :completed_at_not_null, {:checked => @show_only_completed, 'ng-model' => 'q.completed_at_not_null'}, '1', '' + = t(:show_only_complete_orders) + .field.checkbox + %label + = f.check_box :inventory_units_shipment_id_null, {'ng-model' => 'q.inventory_units_shipment_id_null'}, '1', '0' + = t(:show_only_unfulfilled_orders) + .field-block.alpha.eight.columns + = label_tag nil, t(:distributors) + = select_tag("q[distributor_id_in]", + options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]), + {class: "select2 fullwidth", multiple: true, 'ng-model' => 'q.distributor_id_in'}) + .field-block.omega.eight.columns + = label_tag nil, t(:order_cycles) + = select_tag("q[order_cycle_id_in]", + options_for_select(OrderCycle.managed_by(spree_current_user).where('order_cycles.orders_close_at is not null').order('order_cycles.orders_close_at DESC').map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]), + {class: "select2 fullwidth", multiple: true, 'ng-model' => 'q.order_cycle_id_in'}) + .clearfix + .actions.filter-actions + %div + %a.button.icon-search{'ng-click' => 'fetchResults()'} + = t(:filter_results) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index f7f4d90a11..209c67b36e 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -14,57 +14,8 @@ = t(:search) - content_for :table_filter do - %div{"data-hook" => "admin_orders_index_search"} - = search_form_for [:admin, @search] do |f| - .field-block.alpha.four.columns - .date-range-filter.field - = label_tag nil, t(:date_range) - .date-range-fields - = f.text_field :created_at_gt, :class => 'datepicker datepicker-from', :value => params[:q][:created_at_gt], :placeholder => t(:start) - %span.range-divider - %i.icon-arrow-right - = f.text_field :created_at_lt, :class => 'datepicker datepicker-to', :value => params[:q][:created_at_lt], :placeholder => t(:stop) - .field - = label_tag nil, t(:status) - = f.select :state_eq, Spree::Order.state_machines[:state].states.collect {|s| [t("order_state.#{s.name}"), s.value]}, {:include_blank => true}, :class => 'select2' - .four.columns - .field - = label_tag nil, t(:order_number) - = f.text_field :number_cont - .field - = label_tag nil, t(:email) - = f.email_field :email_cont - .four.columns - .field - = label_tag nil, t(:first_name_begins_with) - = f.text_field :bill_address_firstname_start, :size => 25 - .field - = label_tag nil, t(:last_name_begins_with) - = f.text_field :bill_address_lastname_start, :size => 25 - .omega.four.columns - .field.checkbox - %label - = f.check_box :completed_at_not_null, {:checked => @show_only_completed}, '1', '' - = t(:show_only_complete_orders) - .field.checkbox - %label - = f.check_box :inventory_units_shipment_id_null, { }, '1', '0' - = t(:show_only_unfulfilled_orders) - .field-block.alpha.eight.columns - = label_tag nil, t(:distributors) - = select_tag("q[distributor_id_in]", - options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]), - {class: "select2 fullwidth", multiple: true}) - .field-block.omega.eight.columns - = label_tag nil, t(:order_cycles) - = select_tag("q[order_cycle_id_in]", - options_for_select(OrderCycle.managed_by(spree_current_user).where('order_cycles.orders_close_at is not null').order('order_cycles.orders_close_at DESC').map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]), - {class: "select2 fullwidth", multiple: true}) - .clearfix - .actions.filter-actions - %div{"data-hook" => "admin_orders_index_search_buttons"} - = button t(:filter_results), 'icon-search' - + = render partial: 'filters' + %table#listing_orders.index.responsive{"data-hook" => "", width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } %colgroup %col{style: "width: 10%"} From 9da6a5a9b38fe04a19c8a85b890ddc9768599e9b Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 17 Sep 2018 12:13:57 +0100 Subject: [PATCH 096/190] Add column sorting to table --- .../controllers/orders_controller.js.coffee | 11 ++++- app/views/spree/admin/orders/index.html.haml | 48 +++++++++++++++---- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 21a5d42b91..8382eb0332 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $compile, $attrs, Orders) -> +angular.module("admin.orders").controller "ordersCtrl", ($scope, $injector, RequestMonitor, $compile, $attrs, Orders, SortOptions) -> $scope.$compile = $compile $scope.shops = shops $scope.orderCycles = orderCycles @@ -9,6 +9,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $scope.RequestMonitor = RequestMonitor $scope.pagination = Orders.pagination $scope.orders = Orders.all + $scope.sortOptions = SortOptions $scope.initialise = -> $scope.q = { @@ -30,10 +31,18 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, 'q[distributor_id_in]': $scope['q']['distributor_id_in'], 'q[order_cycle_id_in]': $scope['q']['order_cycle_id_in'], 'q[order_cycle_id_in]': $scope['q']['order_cycle_id_in'], + 'q[s]': $scope.sorting || 'id desc', per_page: $scope.per_page || 15, page: page }) + $scope.$watch 'sortOptions', (sort) -> + if sort.predicate != "" + $scope.sorting = sort.predicate + ' desc' if sort.reverse + $scope.sorting = sort.predicate + ' asc' if !sort.reverse + $scope.fetchResults() + , true + $scope.validOrderCycle = (oc) -> $scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 209c67b36e..35c0ad5009 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -24,15 +24,47 @@ %th = t(:products_distributor) - if @show_only_completed - %th= sort_link @search, :completed_at, t(:completed_at, :scope => 'activerecord.attributes.spree/order') + %th + %a{'ng-click' => "sortOptions.toggle('completed_at')"} + = t(:completed_at, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == 'completed_at asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == 'completed_at desc' || sorting === undefined"}= "▼".html_safe - else - %th= sort_link @search, :created_at, t(:created_at, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :number, t(:number, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :state, t(:state, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :payment_state, t(:payment_state, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :shipment_state, t(:shipment_state, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :email, t(:email, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :total, t(:total, :scope => 'activerecord.attributes.spree/order') + %th + %a{'ng-click' => "sortOptions.toggle('created_at')"} + = t(:created_at, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == 'created_at asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == 'created_at desc'"}= "▼".html_safe + %th + %a{'ng-click' => "sortOptions.toggle('number')"} + = t(:number, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == 'number asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == 'number desc'"}= "▼".html_safe + %th + %a{'ng-click' => "sortOptions.toggle('state')"} + = t(:state, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == 'state asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == 'state desc'"}= "▼".html_safe + %th + %a{'ng-click' => "sortOptions.toggle('payment_state')"} + = t(:payment_state, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == 'payment_state asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == 'payment_state desc'"}= "▼".html_safe + %th + %a{'ng-click' => "sortOptions.toggle('shipment_state')"} + = t(:shipment_state, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == 'shipment_state asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == 'shipment_state desc'"}= "▼".html_safe + %th + %a{'ng-click' => "sortOptions.toggle('email')"} + = t(:email, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == 'email asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == 'email desc'"}= "▼".html_safe + %th + %a{'ng-click' => "sortOptions.toggle('total')"} + = t(:total, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == 'total asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == 'total desc'"}= "▼".html_safe %th.actions{"data-hook" => "admin_orders_index_header_actions"} %tbody %tr{ng: {repeat: 'order in orders track by $index', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'", "data-hook" => "admin_orders_index_rows"} From 68f0c800168c636afea92eedeb6b10eb7f416e96 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 17 Sep 2018 12:45:26 +0100 Subject: [PATCH 097/190] Add loading message and spinner --- app/assets/stylesheets/admin/orders.css.scss | 12 ++++++++++++ app/views/spree/admin/orders/index.html.haml | 8 ++++++++ config/locales/en.yml | 1 + 3 files changed, 21 insertions(+) diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 33c5a4a3e8..0955d9fd46 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -91,3 +91,15 @@ th.actions { table.index td.actions { text-align: left; } + +.orders-loading { + margin-top: 1em; + + img { + width: 85px; + } + + span { + font-size: 1.2em; + } +} diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 35c0ad5009..1191846869 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -101,6 +101,14 @@ %div{'ng-if' => 'order.pending_payments'} %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t(:capture)} +.orders-loading{'ng-show' => 'RequestMonitor.loading'} + .row + .small-12.columns.fullwidth.text-center + %img.spinner{ src: "/assets/spinning-circles.svg" } + .row + .small-12.columns.fullwidth.text-center + %span= t(:loading) + %div{'ng-show' => "!RequestMonitor.loading && orders.length > 0" } = render partial: 'angular_pagination' diff --git a/config/locales/en.yml b/config/locales/en.yml index 16a85986b1..e0a9863c03 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -622,6 +622,7 @@ en: last: "Last" previous: "Previous" next: "Next" + loading: "Loading" invoice_email_sent: 'Invoice email has been sent' order_email_resent: 'Order email has been resent' bulk_management: From b2551b4e0bd0574ea2d417c32c4100b829e1dd34 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 17 Sep 2018 16:34:52 +0100 Subject: [PATCH 098/190] Rewrite existing specs --- .../controllers/orders_controller.js.coffee | 2 +- .../admin/orders_controller_decorator.rb | 19 ++++++++--------- .../spree/admin/orders_controller_spec.rb | 21 ++++++++++--------- spec/features/admin/adjustments_spec.rb | 16 +++++++------- .../admin/bulk_order_management_spec.rb | 4 ++-- spec/features/admin/orders_spec.rb | 2 +- .../line_items_controller_spec.js.coffee | 4 ++-- .../orders_controller_spec.js.coffee | 18 +++++++++++++--- .../orders/services/orders_spec.js.coffee | 6 +++--- 9 files changed, 52 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 8382eb0332..1ce3da7595 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -37,7 +37,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, $injector, Requ }) $scope.$watch 'sortOptions', (sort) -> - if sort.predicate != "" + if sort && sort.predicate != "" $scope.sorting = sort.predicate + ' desc' if sort.reverse $scope.sorting = sort.predicate + ' asc' if !sort.reverse $scope.fetchResults() diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index 8c0c914d9e..6df8943966 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -107,17 +107,16 @@ Spree::Admin::OrdersController.class_eval do private def orders - @search = if json_request? - OpenFoodNetwork::Permissions.new(spree_current_user).editable_orders.ransack(params[:q]) - else - Spree::Order.accessible_by(current_ability, :index).ransack(params[:q]) - end + if json_request? + @search = OpenFoodNetwork::Permissions.new(spree_current_user).editable_orders.ransack(params[:q]) + else + @search = Spree::Order.accessible_by(current_ability, :index).ransack(params[:q]) - # Replaced this search to filter orders to only show those distributed by current user (or all for admin user) - @search.result.includes([:user, :shipments, :payments]). - distributed_by_user(spree_current_user). - page(params[:page]). - per(params[:per_page] || Spree::Config[:orders_per_page]) + # Replaced this search to filter orders to only show those distributed by current user (or all for admin user) + @search.result.includes([:user, :shipments, :payments]).distributed_by_user(spree_current_user) + end + + @search.result.page(params[:page]).per(params[:per_page] || Spree::Config[:orders_per_page]) end def require_distributor_abn diff --git a/spec/controllers/spree/admin/orders_controller_spec.rb b/spec/controllers/spree/admin/orders_controller_spec.rb index 25c2683d4d..8d76ea3a8d 100644 --- a/spec/controllers/spree/admin/orders_controller_spec.rb +++ b/spec/controllers/spree/admin/orders_controller_spec.rb @@ -66,26 +66,27 @@ describe Spree::Admin::OrdersController, type: :controller do end it "retrieves a list of orders with appropriate attributes, including line items with appropriate attributes" do - keys = json_response.first.keys.map{ |key| key.to_sym } + keys = json_response['orders'].first.keys.map{ |key| key.to_sym } order_attributes.all?{ |attr| keys.include? attr }.should == true end - it "sorts orders in ascending id order" do - ids = json_response.map{ |order| order['id'] } - ids[0].should < ids[1] - ids[1].should < ids[2] + it "sorts orders in descending id order" do + ids = json_response['orders'].map{ |order| order['id'] } + ids[0].should > ids[1] + ids[1].should > ids[2] end it "formats completed_at to 'yyyy-mm-dd hh:mm'" do - json_response.map{ |order| order['completed_at'] }.all?{ |a| a.match("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$") }.should == true + pp json_response + json_response['orders'].map{ |order| order['completed_at'] }.all?{ |a| a == order1.completed_at.strftime('%B %d, %Y') }.should == true end it "returns distributor object with id key" do - json_response.map{ |order| order['distributor'] }.all?{ |d| d.has_key?('id') }.should == true + json_response['orders'].map{ |order| order['distributor'] }.all?{ |d| d.has_key?('id') }.should == true end it "retrieves the order number" do - json_response.map{ |order| order['number'] }.all?{ |number| number.match("^R\\d{5,10}$") }.should == true + json_response['orders'].map{ |order| order['number'] }.all?{ |number| number.match("^R\\d{5,10}$") }.should == true end end @@ -120,7 +121,7 @@ describe Spree::Admin::OrdersController, type: :controller do end it "retrieves a list of orders" do - keys = json_response.first.keys.map{ |key| key.to_sym } + keys = json_response['orders'].first.keys.map{ |key| key.to_sym } order_attributes.all?{ |attr| keys.include? attr }.should == true end end @@ -132,7 +133,7 @@ describe Spree::Admin::OrdersController, type: :controller do end it "retrieves a list of orders" do - keys = json_response.first.keys.map{ |key| key.to_sym } + keys = json_response['orders'].first.keys.map{ |key| key.to_sym } order_attributes.all?{ |attr| keys.include? attr }.should == true end end diff --git a/spec/features/admin/adjustments_spec.rb b/spec/features/admin/adjustments_spec.rb index a051d28b2b..992ee7c47c 100644 --- a/spec/features/admin/adjustments_spec.rb +++ b/spec/features/admin/adjustments_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" feature %q{ As an administrator I want to manage adjustments on orders -} do +}, js: true do include AuthenticationWorkflow include WebHelper @@ -30,7 +30,7 @@ feature %q{ click_link 'New Adjustment' fill_in 'adjustment_amount', with: 110 fill_in 'adjustment_label', with: 'Late fee' - select 'GST', from: 'tax_rate_id' + select2_select 'GST', from: 'tax_rate_id' click_button 'Continue' # Then I should see the adjustment, with the correct tax @@ -51,11 +51,11 @@ feature %q{ page.find('tr', text: 'Shipping').find('a.icon-edit').click # Then I should see the uneditable included tax and our tax rate as the default - page.should have_field :adjustment_included_tax, with: '10.00', disabled: true - page.should have_select :tax_rate_id, selected: 'GST' + expect(page).to have_field :adjustment_included_tax, with: '10.00', disabled: true + expect(page).to have_select2 :tax_rate_id, selected: 'GST' # When I edit the adjustment, removing the tax - select 'Remove tax', from: :tax_rate_id + select2_select 'Remove tax', from: :tax_rate_id click_button 'Continue' # Then the adjustment tax should be cleared @@ -75,11 +75,11 @@ feature %q{ page.find('tr', text: 'Shipping').find('a.icon-edit').click # Then I should see the uneditable included tax and 'Remove tax' as the default tax rate - page.should have_field :adjustment_included_tax, with: '0.00', disabled: true - page.should have_select :tax_rate_id, selected: [] + expect(page).to have_field :adjustment_included_tax, with: '0.00', disabled: true + expect(page).to have_select2 :tax_rate_id, selected: [] # When I edit the adjustment, setting a tax rate - select 'GST', from: :tax_rate_id + select2_select 'GST', from: :tax_rate_id click_button 'Continue' # Then the adjustment tax should be recalculated diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 602fb99f8c..0c436014b2 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -54,8 +54,8 @@ feature %q{ it "displays a column for order date" do expect(page).to have_selector "th.date", text: "ORDER DATE", :visible => true - expect(page).to have_selector "td.date", text: o1.completed_at.strftime("%F %T"), :visible => true - expect(page).to have_selector "td.date", text: o2.completed_at.strftime("%F %T"), :visible => true + expect(page).to have_selector "td.date", text: o1.completed_at.strftime('%B %d, %Y'), :visible => true + expect(page).to have_selector "td.date", text: o2.completed_at.strftime('%B %d, %Y'), :visible => true end it "displays a column for producer" do diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 0b9a6cb602..99fa8ebb23 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -109,7 +109,7 @@ feature %q{ quick_login_as_admin visit '/admin/orders' uncheck 'Only show complete orders' - click_button 'Filter Results' + page.find('a.icon-search').click click_edit diff --git a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee index 7e63ba7284..1b23dea59d 100644 --- a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee @@ -37,7 +37,7 @@ describe "LineItemsCtrl", -> order = { id: 9, order_cycle: { id: 4 }, distributor: { id: 5 }, number: "R123456" } lineItem = { id: 7, quantity: 3, order: { id: 9 }, supplier: { id: 1 } } - httpBackend.expectGET("/admin/orders.json?q%5Bcompleted_at_gteq%5D=SomeDate&q%5Bcompleted_at_lt%5D=SomeDate&q%5Bcompleted_at_not_null%5D=true&q%5Bstate_not_eq%5D=canceled").respond [order] + httpBackend.expectGET("/admin/orders.json?q%5Bcompleted_at_gteq%5D=SomeDate&q%5Bcompleted_at_lt%5D=SomeDate&q%5Bcompleted_at_not_null%5D=true&q%5Bstate_not_eq%5D=canceled").respond {orders: [order], pagination: {page: 1, pages: 1, results: 1}} httpBackend.expectGET("/admin/bulk_line_items.json?q%5Border%5D%5Bcompleted_at_gteq%5D=SomeDate&q%5Border%5D%5Bcompleted_at_lt%5D=SomeDate&q%5Border%5D%5Bcompleted_at_not_null%5D=true&q%5Border%5D%5Bstate_not_eq%5D=canceled").respond [lineItem] httpBackend.expectGET("/admin/enterprises/visible.json?ams_prefix=basic&q%5Bsells_in%5D%5B%5D=own&q%5Bsells_in%5D%5B%5D=any").respond [distributor] httpBackend.expectGET("/admin/order_cycles.json?ams_prefix=basic&as=distributor&q%5Borders_close_at_gt%5D=SomeDate").respond [orderCycle] @@ -68,7 +68,7 @@ describe "LineItemsCtrl", -> describe "initialisation", -> it "gets suppliers", -> - expect(scope.suppliers).toDeepEqual [supplier ] + expect(scope.suppliers).toDeepEqual [ supplier ] it "gets distributors", -> expect(scope.distributors).toDeepEqual [ distributor ] diff --git a/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee b/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee index 4ceeea277b..0324d79182 100644 --- a/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee @@ -7,13 +7,25 @@ describe "ordersCtrl", -> {id: 10, name: 'Ten', status: 'open', distributors: [{id: 1, name: 'One'}]} {id: 20, name: 'Twenty', status: 'closed', distributors: [{id: 2, name: 'Two', status: 'closed'}]} ] + SortOptions = { + predicate: "", + reverse: false + } beforeEach -> scope = {} + shops = [] + orderCycles = [ + {id: 10, name: 'Ten', status: 'open', distributors: [{id: 1, name: 'One'}]} + {id: 20, name: 'Twenty', status: 'closed', distributors: [{id: 2, name: 'Two', status: 'closed'}]} + ] - module('admin.orders') - inject ($controller) -> - ctrl = $controller 'ordersCtrl', {$scope: scope, $attrs: attrs, shops: shops, orderCycles: orderCycles} + module 'admin.orders', ($provide)-> + $provide.provider('shops', shops) + $provide.provider('orderCycles', orderCycles) + inject (_$injector_, $controller) -> + $injector = _$injector_ + ctrl = $controller 'ordersCtrl', {$scope: scope, $attrs: attrs, $injector: $injector, SortOptions: SortOptions} it "initialises name_and_status", -> expect(scope.orderCycles[0].name_and_status).toEqual "Ten (open)" diff --git a/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee b/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee index 3d41ded40a..bb22689d73 100644 --- a/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee +++ b/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee @@ -18,17 +18,17 @@ describe "Orders service", -> result = response = null beforeEach -> - response = [{ id: 5, name: 'Order 1'}] + response = { orders: [{ id: 5, name: 'Order 1'}], pagination: {page: 1, pages: 1, results: 1} } $httpBackend.expectGET('/admin/orders.json').respond 200, response result = Orders.index() $httpBackend.flush() it "stores returned data in @byID, with ids as keys", -> # OrderResource returns instances of Resource rather than raw objects - expect(Orders.byID).toDeepEqual { 5: response[0] } + expect(Orders.byID).toDeepEqual { 5: response.orders[0] } it "stores returned data in @pristineByID, with ids as keys", -> - expect(Orders.pristineByID).toDeepEqual { 5: response[0] } + expect(Orders.pristineByID).toDeepEqual { 5: response.orders[0] } it "returns an array of orders", -> expect(result).toDeepEqual response From 6768055b4ddadff70f63f0c67ca4b010c61360f1 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 18 Sep 2018 01:09:31 +0100 Subject: [PATCH 099/190] Split orders into 2 angular controllers --- .../controllers/order_controller.js.coffee | 26 +++++++++++++++++ .../controllers/orders_controller.js.coffee | 28 +------------------ app/views/spree/admin/orders/edit.html.haml | 2 +- app/views/spree/admin/orders/new.html.haml | 2 +- .../admin/orders/set_distribution.html.haml | 2 +- ...coffee => order_controller_spec.js.coffee} | 20 +++---------- 6 files changed, 34 insertions(+), 46 deletions(-) create mode 100644 app/assets/javascripts/admin/orders/controllers/order_controller.js.coffee rename spec/javascripts/unit/admin/orders/controllers/{orders_controller_spec.js.coffee => order_controller_spec.js.coffee} (66%) diff --git a/app/assets/javascripts/admin/orders/controllers/order_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/order_controller.js.coffee new file mode 100644 index 0000000000..822e81eb49 --- /dev/null +++ b/app/assets/javascripts/admin/orders/controllers/order_controller.js.coffee @@ -0,0 +1,26 @@ +angular.module("admin.orders").controller "orderCtrl", ($scope, shops, orderCycles, $compile, $attrs, Orders) -> + $scope.$compile = $compile + $scope.shops = shops + $scope.orderCycles = orderCycles + + $scope.distributor_id = parseInt($attrs.ofnDistributorId) + $scope.order_cycle_id = parseInt($attrs.ofnOrderCycleId) + + $scope.validOrderCycle = (oc) -> + $scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id) + + $scope.distributorHasOrderCycles = (distributor) -> + (oc for oc in $scope.orderCycles when @orderCycleHasDistributor(oc, distributor.id)).length > 0 + + $scope.orderCycleHasDistributor = (oc, distributor_id) -> + distributor_ids = (d.id for d in oc.distributors) + distributor_ids.indexOf(distributor_id) != -1 + + $scope.distributionChosen = -> + $scope.distributor_id && $scope.order_cycle_id + + for oc in $scope.orderCycles + oc.name_and_status = "#{oc.name} (#{oc.status})" + + for shop in $scope.shops + shop.disabled = !$scope.distributorHasOrderCycles(shop) diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 1ce3da7595..96bfd63ab2 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -1,11 +1,4 @@ -angular.module("admin.orders").controller "ordersCtrl", ($scope, $injector, RequestMonitor, $compile, $attrs, Orders, SortOptions) -> - $scope.$compile = $compile - $scope.shops = shops - $scope.orderCycles = orderCycles - - $scope.distributor_id = parseInt($attrs.ofnDistributorId) - $scope.order_cycle_id = parseInt($attrs.ofnOrderCycleId) - +angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, Orders, SortOptions) -> $scope.RequestMonitor = RequestMonitor $scope.pagination = Orders.pagination $scope.orders = Orders.all @@ -43,25 +36,6 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, $injector, Requ $scope.fetchResults() , true - $scope.validOrderCycle = (oc) -> - $scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id) - - $scope.distributorHasOrderCycles = (distributor) -> - (oc for oc in orderCycles when @orderCycleHasDistributor(oc, distributor.id)).length > 0 - - $scope.orderCycleHasDistributor = (oc, distributor_id) -> - distributor_ids = (d.id for d in oc.distributors) - distributor_ids.indexOf(distributor_id) != -1 - - $scope.distributionChosen = -> - $scope.distributor_id && $scope.order_cycle_id - $scope.changePage = (newPage) -> $scope.page = newPage $scope.fetchResults(newPage) - - for oc in $scope.orderCycles - oc.name_and_status = "#{oc.name} (#{oc.status})" - - for shop in $scope.shops - shop.disabled = !$scope.distributorHasOrderCycles(shop) diff --git a/app/views/spree/admin/orders/edit.html.haml b/app/views/spree/admin/orders/edit.html.haml index f53cca42ef..f897065224 100644 --- a/app/views/spree/admin/orders/edit.html.haml +++ b/app/views/spree/admin/orders/edit.html.haml @@ -13,7 +13,7 @@ %div{"data-hook" => "admin_order_edit_header"} = render 'spree/shared/error_messages', target: @order -%div{"ng-app" => "admin.orders", "ng-controller" => "ordersCtrl", "ofn-distributor-id" => @order.distributor_id, "ofn-order-cycle-id" => @order.order_cycle_id} +%div{"ng-app" => "admin.orders", "ng-controller" => "orderCtrl", "ofn-distributor-id" => @order.distributor_id, "ofn-order-cycle-id" => @order.order_cycle_id} = render 'add_product' %div{"data-hook" => "admin_order_edit_form"} diff --git a/app/views/spree/admin/orders/new.html.haml b/app/views/spree/admin/orders/new.html.haml index 24190f3190..b653439230 100644 --- a/app/views/spree/admin/orders/new.html.haml +++ b/app/views/spree/admin/orders/new.html.haml @@ -14,7 +14,7 @@ %div{"data-hook" => "admin_order_new_header"} = render 'spree/shared/error_messages', :target => @order -%div{"ng-app" => "admin.orders", "ng-controller" => "ordersCtrl"} +%div{"ng-app" => "admin.orders", "ng-controller" => "orderCtrl"} %div{"ng-show" => "distributionChosen()"} = render 'add_product' diff --git a/app/views/spree/admin/orders/set_distribution.html.haml b/app/views/spree/admin/orders/set_distribution.html.haml index 51c7b97e8d..89d036b93a 100644 --- a/app/views/spree/admin/orders/set_distribution.html.haml +++ b/app/views/spree/admin/orders/set_distribution.html.haml @@ -14,7 +14,7 @@ %div{"data-hook" => "admin_order_new_header"} = render 'spree/shared/error_messages', :target => @order -%div{"ng-app" => "admin.orders", "ng-controller" => "ordersCtrl"} +%div{"ng-app" => "admin.orders", "ng-controller" => "orderCtrl"} = form_for @order, url: admin_order_url(@order), method: :put do |f| = render 'spree/admin/orders/_form/distribution_fields' -# This param passed to stop validation error in next page due to no line items in order yet: diff --git a/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee b/spec/javascripts/unit/admin/orders/controllers/order_controller_spec.js.coffee similarity index 66% rename from spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee rename to spec/javascripts/unit/admin/orders/controllers/order_controller_spec.js.coffee index 0324d79182..297eac32f6 100644 --- a/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/orders/controllers/order_controller_spec.js.coffee @@ -1,4 +1,4 @@ -describe "ordersCtrl", -> +describe "orderCtrl", -> ctrl = null scope = {} attrs = {} @@ -7,25 +7,13 @@ describe "ordersCtrl", -> {id: 10, name: 'Ten', status: 'open', distributors: [{id: 1, name: 'One'}]} {id: 20, name: 'Twenty', status: 'closed', distributors: [{id: 2, name: 'Two', status: 'closed'}]} ] - SortOptions = { - predicate: "", - reverse: false - } beforeEach -> scope = {} - shops = [] - orderCycles = [ - {id: 10, name: 'Ten', status: 'open', distributors: [{id: 1, name: 'One'}]} - {id: 20, name: 'Twenty', status: 'closed', distributors: [{id: 2, name: 'Two', status: 'closed'}]} - ] - module 'admin.orders', ($provide)-> - $provide.provider('shops', shops) - $provide.provider('orderCycles', orderCycles) - inject (_$injector_, $controller) -> - $injector = _$injector_ - ctrl = $controller 'ordersCtrl', {$scope: scope, $attrs: attrs, $injector: $injector, SortOptions: SortOptions} + module 'admin.orders' + inject ($controller) -> + ctrl = $controller 'orderCtrl', {$scope: scope, $attrs: attrs, shops: shops, orderCycles: orderCycles} it "initialises name_and_status", -> expect(scope.orderCycles[0].name_and_status).toEqual "Ten (open)" From 64620c279749c3c5c35e1355ad44091833b7cb92 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 18 Sep 2018 01:16:37 +0100 Subject: [PATCH 100/190] Tidy up response formats for easier testing --- .../javascripts/admin/resources/services/orders.js.coffee | 2 +- .../unit/admin/orders/services/orders_spec.js.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/resources/services/orders.js.coffee b/app/assets/javascripts/admin/resources/services/orders.js.coffee index 332dc44796..5eef10eecb 100644 --- a/app/assets/javascripts/admin/resources/services/orders.js.coffee +++ b/app/assets/javascripts/admin/resources/services/orders.js.coffee @@ -10,7 +10,7 @@ angular.module("admin.resources").factory 'Orders', ($q, OrderResource, RequestM @load(data) (callback || angular.noop)(data) RequestMonitor.load(request.$promise) - request + @all load: (data) -> angular.extend(@pagination, data.pagination) diff --git a/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee b/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee index bb22689d73..68bc3beb1f 100644 --- a/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee +++ b/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee @@ -31,7 +31,7 @@ describe "Orders service", -> expect(Orders.pristineByID).toDeepEqual { 5: response.orders[0] } it "returns an array of orders", -> - expect(result).toDeepEqual response + expect(result).toDeepEqual response.orders describe "#save", -> From 9ce32e3c141d9f7101c8f5b7fbe9f8bc5bf54e46 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 18 Sep 2018 09:51:11 +0100 Subject: [PATCH 101/190] Add new ordersCtrl spec --- .../orders_controller_spec.js.coffee | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee diff --git a/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee b/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee new file mode 100644 index 0000000000..4aed599140 --- /dev/null +++ b/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee @@ -0,0 +1,37 @@ +describe "ordersCtrl", -> + ctrl = null + Orders = null + $scope = null + orders = [ + { id: 8, order_cycle: { id: 4 }, distributor: { id: 5 }, number: "R123456" } + { id: 9, order_cycle: { id: 5 }, distributor: { id: 7 }, number: "R213776" } + ] + form = { + q: { + created_at_lt: '' + created_at_gt: '' + completed_at_not_null: true + } + } + + beforeEach -> + module 'admin.orders' + inject ($controller, $rootScope, RequestMonitor, SortOptions) -> + $scope = $rootScope.$new() + Orders = + index: jasmine.createSpy('index').and.returnValue(orders) + all: orders + ctrl = $controller 'ordersCtrl', { $scope: $scope, RequestMonitor: RequestMonitor, SortOptions: SortOptions, Orders: Orders } + $scope.q = form.q + + describe "initialising the controller", -> + it "fetches orders", -> + $scope.initialise() + expect(Orders.index).toHaveBeenCalled() + expect($scope.orders).toEqual orders + + describe "using pagination", -> + it "changes the page", -> + $scope.changePage(2) + expect($scope.page).toEqual 2 + expect(Orders.index).toHaveBeenCalled() From ba254802f814ce378ddd5d42f2b4153e1c2d450c Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Wed, 19 Sep 2018 09:27:44 +0100 Subject: [PATCH 102/190] Move angular_pagination to /views/admin/shared/ --- .../admin/orders => admin/shared}/_angular_pagination.html.haml | 0 app/views/spree/admin/orders/index.html.haml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/views/{spree/admin/orders => admin/shared}/_angular_pagination.html.haml (100%) diff --git a/app/views/spree/admin/orders/_angular_pagination.html.haml b/app/views/admin/shared/_angular_pagination.html.haml similarity index 100% rename from app/views/spree/admin/orders/_angular_pagination.html.haml rename to app/views/admin/shared/_angular_pagination.html.haml diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 1191846869..750a7d7958 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -110,7 +110,7 @@ %span= t(:loading) %div{'ng-show' => "!RequestMonitor.loading && orders.length > 0" } - = render partial: 'angular_pagination' + = render partial: 'admin/shared/angular_pagination' .no-objects-found{'ng-show' => "!RequestMonitor.loading && orders.length == 0"} = t(:no_orders_found) From 3cbb576b4f8e0ff150b9e30f076ab39f3d2ca9d1 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Wed, 19 Sep 2018 10:21:01 +0100 Subject: [PATCH 103/190] Move payment object logic out of order serializer and delete code --- app/serializers/api/admin/order_serializer.rb | 30 +++---------------- app/services/pending_payments.rb | 15 ++++++++++ app/views/spree/admin/orders/index.html.haml | 2 +- 3 files changed, 20 insertions(+), 27 deletions(-) create mode 100644 app/services/pending_payments.rb diff --git a/app/serializers/api/admin/order_serializer.rb b/app/serializers/api/admin/order_serializer.rb index 065025d22f..7b7c5018c1 100644 --- a/app/serializers/api/admin/order_serializer.rb +++ b/app/serializers/api/admin/order_serializer.rb @@ -2,7 +2,7 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer attributes :id, :number, :full_name, :email, :phone, :completed_at, :display_total attributes :show_path, :edit_path, :state, :payment_state, :shipment_state attributes :payments_path, :shipments_path, :ship_path, :ready_to_ship, :created_at - attributes :distributor_name, :special_instructions, :pending_payments, :capture_path + attributes :distributor_name, :special_instructions, :capture_path has_one :distributor, serializer: Api::Admin::IdSerializer has_one :order_cycle, serializer: Api::Admin::IdSerializer @@ -40,9 +40,9 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer end def capture_path - return '' unless ready_for_payment? - return unless payment_to_capture - Spree::Core::Engine.routes_url_helpers.fire_admin_order_payment_path(object, payment_to_capture.id, e: 'capture') + payment_due = PendingPayments.new(object) + return '' unless object.payment_required? && payment_due.payment_object + Spree::Core::Engine.routes_url_helpers.fire_admin_order_payment_path(object, payment_due.payment_object.id, e: 'capture') end def ready_to_ship @@ -68,26 +68,4 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer def completed_at object.completed_at.blank? ? "" : I18n.l(object.completed_at, format: '%B %d, %Y') end - - def pending_payments - return if object.payments.blank? - payment = object.payments.select{ |p| p if p.state == 'checkout' }.first - return unless can_be_captured? payment - - payment.id - end - - private - - def ready_for_payment? - object.payment_required? && object.payments.present? - end - - def payment_to_capture - object.payments.select{ |p| p if p.state == 'checkout' }.first - end - - def can_be_captured?(payment) - payment && payment.actions.include?('capture') - end end diff --git a/app/services/pending_payments.rb b/app/services/pending_payments.rb new file mode 100644 index 0000000000..729af49e47 --- /dev/null +++ b/app/services/pending_payments.rb @@ -0,0 +1,15 @@ +# Returns the capturable payment object for an order with balance due + +class PendingPayments + def initialize(order) + @order = order + end + + def payment_object + @order.payments.select{ |p| p if p.state == 'checkout' }.first + end + + def can_be_captured? + payment_object && payment_object.actions.include?('capture') + end +end diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 750a7d7958..ed94800e02 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -98,7 +98,7 @@ %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{::order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t(:edit)} %div{'ng-if' => 'order.ready_to_ship'} %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{::order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t(:ship)} - %div{'ng-if' => 'order.pending_payments'} + %div{'ng-if' => 'order.capture_path'} %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t(:capture)} .orders-loading{'ng-show' => 'RequestMonitor.loading'} From 454cd8bfbf45575764188bee33e4a78520ca7f36 Mon Sep 17 00:00:00 2001 From: Transifex-Openfoodnetwork Date: Wed, 19 Sep 2018 22:30:06 +1000 Subject: [PATCH 104/190] Updating translations for config/locales/nb.yml --- config/locales/nb.yml | 93 ++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/config/locales/nb.yml b/config/locales/nb.yml index bfb7ad6468..51d7efc2c5 100644 --- a/config/locales/nb.yml +++ b/config/locales/nb.yml @@ -64,6 +64,9 @@ nb: user_passwords: spree_user: updated_not_active: "Ditt passord har blitt tilbakestilt, men epostadressen din er ikke bekreftet enda." + models: + order_cycle: + cloned_order_cycle_name: "KOPI AV %{order_cycle}" enterprise_mailer: confirmation_instructions: subject: "Vennligst bekreft epostadressen til %{enterprise}" @@ -71,9 +74,26 @@ nb: subject: "%{enterprise} er nå på %{sitename}" invite_manager: subject: "%{enterprise} har invitert deg til å være en administrator" + order_mailer: + cancel_email: + dear_customer: "Kjære Kunde," + instructions: "Din bestilling har blitt KANSELLERT. Vennligst behold denne kanselleringsinformasjonen som referanse." + order_summary_canceled: "Bestillingssammendrag [KANSELLERT]" + subject: "Kansellering av bestilling" + subtotal: "Subtotal: %{subtotal}" + total: "Ordre Totalt: %{total}" producer_mailer: order_cycle: subject: "Bestillingsrunderapport for %{producer}" + shipment_mailer: + shipped_email: + dear_customer: "Kjære Kunde," + instructions: "Din bestilling har blitt sendt" + shipment_summary: "Leveringssammendrag" + subject: "Leveringsvarsling" + thanks: "Takk for handelen." + track_information: "Sporingsinformasjon: %{tracking}" + track_link: "Sporingslink: %{url}" subscription_mailer: placement_summary_email: subject: Et sammendrag av nylig bestilte abonnementsbestillinger @@ -136,6 +156,7 @@ nb: free_trial: "gratis prøveperiode" plus_tax: "pluss MVA" min_bill_turnover_desc: "når omsettning overstiger %{mbt_amount}" + more: "Mer" say_no: "Nei" say_yes: "Ja" then: vil @@ -382,6 +403,7 @@ nb: main_links: 'Lenker Hovedmeny ' footer_and_external_links: Footer og eksterne lenker your_content: Ditt innhold + user_guide: Brukermanual enterprise_fees: index: title: Bedriftsavgifter @@ -766,6 +788,9 @@ nb: search_placeholder: Søk på navn manage: Administrer manage_link: Innstillinger + producer?: "Produsent?" + package: "Pakke" + status: "Status" new_form: owner: Eier owner_tip: Primærbrukeren ansvarlig for denne bedriften. @@ -777,6 +802,14 @@ nb: new: title: Ny Bedrift back_link: Tilbake til bedriftsliste + remove_logo: + remove: "Fjern Bilde" + removed_successfully: "Logo fjernet vellykket" + immediate_removal_warning: "Logoen vil bli fjernet umiddelbart etter at du har bekreftet." + remove_promo_image: + remove: "Fjern Bilde" + removed_successfully: "Promobilde fjernet vellykket" + immediate_removal_warning: "Promo-bildet vil bli fjernet umiddelbart etter at du har bekreftet." welcome: welcome_title: Velkommen til Open Food Network! welcome_text: Du har opprettet en @@ -809,6 +842,11 @@ nb: save_reload: Lagre og last siden på nytt coordinator_fees: add: Legg til koordinatoravgift + filters: + search_by_order_cycle_name: "Søk etter navn på bestillingsrunde..." + involving: "Involverer" + any_enterprise: "Enhver Bedrift" + any_schedule: "Enhver Tidsplan" form: incoming: Innkommende supplier: Leverandør @@ -822,7 +860,6 @@ nb: delivery_details: Hente-/Leveringsdetaljer debug_info: Debuginformasjon index: - involving: Involverer schedule: Tidsplan schedules: Tidsplaner adding_a_new_schedule: Legge til en ny tidsplan @@ -1845,34 +1882,34 @@ nb: you_have_no_orders_yet: "Du har ingen bestilinger enda" running_balance: "Løpende balanse" outstanding_balance: "Utestående balanse" - admin_entreprise_relationships: "Bedriftsrettigheter" - admin_entreprise_relationships_everything: "Alt" - admin_entreprise_relationships_permits: "tillater" - admin_entreprise_relationships_seach_placeholder: "Søk" - admin_entreprise_relationships_button_create: "Opprett" - admin_entreprise_groups: "Bedriftsgrupper" - admin_entreprise_groups_name: "Navn" - admin_entreprise_groups_owner: "Eier" - admin_entreprise_groups_on_front_page: "På forsiden?" - admin_entreprise_groups_entreprise: "Bedrifter" - admin_entreprise_groups_data_powertip: "Hovedbrukeren ansvarlig for denne gruppen." - admin_entreprise_groups_data_powertip_logo: "Dette er logoen for gruppen" - admin_entreprise_groups_data_powertip_promo_image: "Dette bildet vises på toppen av Gruppens profil" - admin_entreprise_groups_contact: "Kontakt" - admin_entreprise_groups_contact_phone_placeholder: "eks: 987 123 654" - admin_entreprise_groups_contact_address1_placeholder: "eks: Gårdsvei 12" - admin_entreprise_groups_contact_city: "Område" - admin_entreprise_groups_contact_city_placeholder: "f.eks. Nesodden" - admin_entreprise_groups_contact_zipcode: "Postnummer" - admin_entreprise_groups_contact_zipcode_placeholder: "f.eks. 1450" - admin_entreprise_groups_contact_state_id: "Fylke" - admin_entreprise_groups_contact_country_id: "Land" - admin_entreprise_groups_web: "Nettressurser" - admin_entreprise_groups_web_twitter: "f.eks. @alt_lokalt" - admin_entreprise_groups_web_website_placeholder: "f.eks. www.truffles.com" + admin_enterprise_relationships: "Bedriftsrettigheter" + admin_enterprise_relationships_everything: "Alt" + admin_enterprise_relationships_permits: "tillater" + admin_enterprise_relationships_seach_placeholder: "Søk" + admin_enterprise_relationships_button_create: "Opprett" + admin_enterprise_groups: "Bedriftsgrupper" + admin_enterprise_groups_name: "Navn" + admin_enterprise_groups_owner: "Eier" + admin_enterprise_groups_on_front_page: "På forsiden?" + admin_enterprise_groups_enterprise: "Bedrifter" + admin_enterprise_groups_data_powertip: "Hovedbrukeren ansvarlig for denne gruppen." + admin_enterprise_groups_data_powertip_logo: "Dette er logoen for gruppen" + admin_enterprise_groups_data_powertip_promo_image: "Dette bildet vises på toppen av Gruppens profil" + admin_enterprise_groups_contact: "Kontakt" + admin_enterprise_groups_contact_phone_placeholder: "f.eks. 987 123 654" + admin_enterprise_groups_contact_address1_placeholder: "f.eks. 123 High Street" + admin_enterprise_groups_contact_city: "Område" + admin_enterprise_groups_contact_city_placeholder: "f.eks. Northcote" + admin_enterprise_groups_contact_zipcode: "Postnummer" + admin_enterprise_groups_contact_zipcode_placeholder: "f.eks. 3070" + admin_enterprise_groups_contact_state_id: "Tilstand" + admin_enterprise_groups_contact_country_id: "Land" + admin_enterprise_groups_web: "Nettressurser" + admin_enterprise_groups_web_twitter: "f.eks. @the_prof" + admin_enterprise_groups_web_website_placeholder: "f.eks. www.truffles.com" admin_order_cycles: "Administrasjon Bestillingsrunder" open: "Åpne" - close: "Steng" + close: "Lukk" create: "Opprett" search: "Søk" supplier: "Leverandør" @@ -2005,7 +2042,7 @@ nb: report_payment_totals: 'Betalingstotaler' report_all: 'alle' report_order_cycle: "Bestillingsrunde:" - report_entreprises: "Bedrifter:" + report_enterprises: "Bedrifter:" report_users: "Brukere:" report_tax_rates: Avgiftsrater report_tax_types: Avgiftstyper From a2b3d8372edfc1a09ac7d69dd42da3933eddff6b Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 18 Sep 2018 11:18:31 +0200 Subject: [PATCH 105/190] Reduce complexity of OrderUpdater decorator --- app/models/spree/order_updater_decorator.rb | 75 ++++++++++++++------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/app/models/spree/order_updater_decorator.rb b/app/models/spree/order_updater_decorator.rb index 6ef5d2033c..03aba06c9b 100644 --- a/app/models/spree/order_updater_decorator.rb +++ b/app/models/spree/order_updater_decorator.rb @@ -1,41 +1,60 @@ module Spree OrderUpdater.class_eval do - # TODO: This logic adapted from Spree 2.4, remove when we get there - # Handles state updating in a much more logical way than < 2.4 - # Specifically, doesn't depend on payments.last to determine payment state - # Also swapped: == 0 for .zero?, .size == 0 for empty? and .size > 0 for !empty? + # TODO: This logic adapted from Spree 2.4, remove when we get there Handles + # state updating in a much more logical way than < 2.4 Specifically, + # doesn't depend on payments.last to determine payment state Also swapped: + # == 0 for .zero?, .size == 0 for empty? and .size > 0 for !empty? + # # See: # https://github.com/spree/spree/commit/38b8456183d11fc1e00e395e7c9154c76ef65b85 # https://github.com/spree/spree/commit/7b264acff7824f5b3dc6651c106631d8f30b147a def update_payment_state last_payment_state = order.payment_state - if payments.present? && payments.valid.empty? - order.payment_state = 'failed' - elsif order.state == 'canceled' && order.payment_total.zero? - order.payment_state = 'void' - else - # This part added so that we don't need to override order.outstanding_balance - balance = order.outstanding_balance - balance = -1 * order.payment_total if canceled_and_paid_for? - order.payment_state = 'balance_due' if balance > 0 - order.payment_state = 'credit_owed' if balance < 0 - order.payment_state = 'paid' if balance.zero? - - # Original logic - # order.payment_state = 'balance_due' if order.outstanding_balance > 0 - # order.payment_state = 'credit_owed' if order.outstanding_balance < 0 - # order.payment_state = 'paid' if !order.outstanding_balance? - end - + order.payment_state = infer_payment_state track_payment_state_change(last_payment_state) + order.payment_state end private + def infer_payment_state + if failed_payments? + 'failed' + elsif canceled_and_not_paid_for? + 'void' + else + infer_payment_state_from_balance + end + end + + def infer_payment_state_from_balance + # This part added so that we don't need to override + # order.outstanding_balance + balance = order.outstanding_balance + balance = -1 * order.payment_total if canceled_and_paid_for? + + infer_state(balance) + end + + def infer_state(balance) + if balance > 0 + 'balance_due' + elsif balance < 0 + 'credit_owed' + elsif balance.zero? + 'paid' + end + end + + # Tracks the state transition through a state_change for this order. It + # does so until the last state is reached. That is, when the infered next + # state is the same as the order has now. + # + # @param last_payment_state [String] def track_payment_state_change(last_payment_state) - return unless last_payment_state != order.payment_state + return if last_payment_state == order.payment_state order.state_changed('payment') end @@ -45,8 +64,16 @@ module Spree order.canceled? && paid? end + def canceled_and_not_paid_for? + order.state == 'canceled' && order.payment_total.zero? + end + def paid? - order.payments.present? && !order.payments.completed.empty? + payments.present? && !payments.completed.empty? + end + + def failed_payments? + payments.present? && payments.valid.empty? end end end From 0ac16ce0964cc1379f28804beeed17f8871d6e74 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 17 Sep 2018 13:00:44 +0200 Subject: [PATCH 106/190] Get useful feedback from Rubocop Metrics cops One of the biggest pros of linters like Rubocop is to get valuable feedback to help write better code. The way we have Rubocop configured now we don't prevent new code from adhere improved code quality and this is specially important when touching code that already suffers from complexity. Without all Rubocop's Metrics cops enabled there's no way to get this insights and write better code. This enables them while regenerating the `.rubocop_todo.yml` to hide the current violations. So, next time we touch existing code that we think could be simpler, we should go to `.rubocop_todo.yml` and remove any occurrences of the file in question. This way we could Rubocop's feedback right in the editor. This is tremendously helpful when refactoring. It shows you where to start. --- .codeclimate.yml | 16 +-- .rubocop.yml | 22 ++--- .rubocop_todo.yml | 246 +++++++++++++++++++++++++++------------------- Gemfile.lock | 4 +- 4 files changed, 165 insertions(+), 123 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 9646b677e9..60c015c60e 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -2,7 +2,7 @@ version: "2" plugins: rubocop: enabled: true - channel: "rubocop-0-55" + channel: "rubocop-0-57" scss-lint: enabled: true checks: @@ -19,23 +19,23 @@ checks: argument-count: enabled: false complex-logic: - enabled: true + enabled: false file-lines: - enabled: true + enabled: false method-complexity: - enabled: true + enabled: false method-count: enabled: false method-lines: enabled: false nested-control-flow: - enabled: true + enabled: false return-statements: - enabled: true + enabled: false similar-code: - enabled: true + enabled: false identical-code: - enabled: true + enabled: false exclude_patterns: - "spec/**/*" - "vendor/**/*" diff --git a/.rubocop.yml b/.rubocop.yml index 80d4c1bf7e..f62c124ea6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,16 +4,12 @@ inherit_from: AllCops: TargetRubyVersion: 2.1 TargetRailsVersion: 3.2 - Include: - - '**/Rakefile' - - '**/config.ru' Exclude: - 'db/**/*' - 'config/**/*' - 'script/**/*' - 'vendor/**/*' - 'node_modules/**/*' - - !ruby/regexp /old_and_unused\.rb$/ # The parser gem fails to parse this file with out current Ruby version. - 'spec/factories.rb' @@ -174,28 +170,28 @@ Lint/AssignmentInCondition: StyleGuide: http://relaxed.ruby.style/#lintassignmentincondition Metrics/AbcSize: - Enabled: false + Max: 15 Metrics/BlockNesting: - Enabled: false + Max: 3 Metrics/ClassLength: - Enabled: false + Max: 100 Metrics/ModuleLength: - Enabled: false + Max: 100 Metrics/CyclomaticComplexity: - Enabled: false + Max: 6 Metrics/LineLength: - Enabled: false + Max: 80 Metrics/MethodLength: - Enabled: false + Max: 10 Metrics/ParameterLists: - Enabled: false + Max: 5 Metrics/PerceivedComplexity: - Enabled: false + Max: 7 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7798d41309..378129593e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,12 @@ # This configuration was generated by # `rubocop --auto-gen-config --exclude-limit 1400` -# on 2018-08-06 18:22:59 +0800 using RuboCop version 0.55.0. +# on 2018-09-19 19:24:45 +0200 using RuboCop version 0.57.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 35 +# Offense count: 32 # Cop supports --auto-correct. # Configuration parameters: Include, TreatCommentsAsGroupSeparators. # Include: **/*.gemfile, **/Gemfile, **/gems.rb @@ -14,20 +14,20 @@ Bundler/OrderedGems: Exclude: - 'Gemfile' -# Offense count: 116 +# Offense count: 115 # Cop supports --auto-correct. Layout/AlignArray: Exclude: - - 'app/controllers/admin/contents_controller.rb' - 'lib/open_food_network/bulk_coop_report.rb' - 'lib/open_food_network/customers_report.rb' - 'lib/open_food_network/order_and_distributor_report.rb' - 'lib/open_food_network/orders_and_fulfillments_report.rb' - 'lib/open_food_network/packing_report.rb' - - 'spec/controllers/spree/orders_controller_spec.rb' + - 'spec/controllers/cart_controller_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' + - 'spec/services/cart_service_spec.rb' -# Offense count: 127 +# Offense count: 121 # Cop supports --auto-correct. # Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table @@ -35,7 +35,6 @@ Layout/AlignArray: # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit Layout/AlignHash: Exclude: - - 'app/overrides/add_capture_order_shortcut.rb' - 'app/overrides/replace_shipping_address_form_with_distributor_details.rb' - 'lib/open_food_network/bulk_coop_report.rb' - 'lib/open_food_network/orders_and_fulfillments_report.rb' @@ -64,7 +63,6 @@ Layout/AlignHash: # SupportedStyles: with_first_parameter, with_fixed_indentation Layout/AlignParameters: Exclude: - - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/helpers/injection_helper.rb' - 'app/models/enterprise.rb' - 'app/models/enterprise_group.rb' @@ -77,9 +75,11 @@ Layout/AlignParameters: - 'lib/tasks/dev.rake' - 'spec/controllers/enterprises_controller_spec.rb' - 'spec/controllers/shop_controller_spec.rb' + - 'spec/features/admin/enterprise_fees_spec.rb' - 'spec/features/admin/enterprise_relationships_spec.rb' - 'spec/features/admin/order_cycles_spec.rb' - 'spec/features/consumer/shopping/checkout_spec.rb' + - 'spec/features/consumer/shopping/orders_spec.rb' - 'spec/helpers/enterprises_helper_spec.rb' - 'spec/lib/open_food_network/user_balance_calculator_spec.rb' - 'spec/serializers/variant_serializer_spec.rb' @@ -104,9 +104,16 @@ Layout/BlockEndNewline: # Offense count: 1 # Cop supports --auto-correct. +Layout/ClosingHeredocIndentation: + Exclude: + - 'app/models/content_configuration.rb' + +# Offense count: 2 +# Cop supports --auto-correct. Layout/ClosingParenthesisIndentation: Exclude: - - 'spec/features/admin/order_cycles_spec.rb' + - 'spec/controllers/spree/admin/orders/customer_details_controller_spec.rb' + - 'spec/serializers/variant_serializer_spec.rb' # Offense count: 8 # Cop supports --auto-correct. @@ -119,7 +126,7 @@ Layout/ElseAlignment: - 'app/serializers/api/admin/order_cycle_serializer.rb' - 'lib/open_food_network/sales_tax_report.rb' -# Offense count: 205 +# Offense count: 201 # Cop supports --auto-correct. Layout/EmptyLines: Exclude: @@ -162,7 +169,6 @@ Layout/EmptyLines: - 'app/models/spree/line_item_decorator.rb' - 'app/models/spree/option_type_decorator.rb' - 'app/models/spree/option_value_decorator.rb' - - 'app/models/spree/order_populator_decorator.rb' - 'app/models/spree/payment_decorator.rb' - 'app/models/spree/preference_decorator.rb' - 'app/models/spree/preferences/file_configuration.rb' @@ -215,6 +221,7 @@ Layout/EmptyLines: - 'spec/features/admin/order_cycles_spec.rb' - 'spec/features/admin/orders_spec.rb' - 'spec/features/admin/payment_method_spec.rb' + - 'spec/features/admin/product_import_spec.rb' - 'spec/features/admin/products_spec.rb' - 'spec/features/admin/reports_spec.rb' - 'spec/features/admin/shipping_methods_spec.rb' @@ -242,7 +249,6 @@ Layout/EmptyLines: - 'spec/models/product_distribution_spec.rb' - 'spec/models/spree/adjustment_spec.rb' - 'spec/models/spree/line_item_spec.rb' - - 'spec/models/spree/order_populator_spec.rb' - 'spec/models/spree/order_spec.rb' - 'spec/models/spree/product_spec.rb' - 'spec/models/spree/shipping_method_spec.rb' @@ -252,6 +258,7 @@ Layout/EmptyLines: - 'spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb' - 'spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb' - 'spec/serializers/credit_card_serializer_spec.rb' + - 'spec/services/cart_service_spec.rb' - 'spec/support/delayed_job_helper.rb' - 'spec/support/matchers/table_matchers.rb' @@ -261,7 +268,7 @@ Layout/EmptyLinesAroundArguments: Exclude: - 'spec/archive/features/consumer/checkout_spec.rb' -# Offense count: 64 +# Offense count: 61 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, no_empty_lines @@ -281,7 +288,6 @@ Layout/EmptyLinesAroundBlockBody: - 'lib/tasks/users.rake' - 'spec/controllers/admin/order_cycles_controller_spec.rb' - 'spec/controllers/admin/tag_rules_controller_spec.rb' - - 'spec/controllers/cart_controller_spec.rb' - 'spec/controllers/spree/admin/orders_controller_spec.rb' - 'spec/controllers/spree/admin/reports_controller_spec.rb' - 'spec/controllers/spree/api/orders_controller_spec.rb' @@ -292,6 +298,7 @@ Layout/EmptyLinesAroundBlockBody: - 'spec/features/admin/orders_spec.rb' - 'spec/features/admin/reports_spec.rb' - 'spec/features/admin/variant_overrides_spec.rb' + - 'spec/features/consumer/cookies_spec.rb' - 'spec/features/consumer/shopping/embedded_groups_spec.rb' - 'spec/features/consumer/shopping/embedded_shopfronts_spec.rb' - 'spec/features/consumer/shopping/shopping_spec.rb' @@ -307,8 +314,8 @@ Layout/EmptyLinesAroundBlockBody: - 'spec/lib/open_food_network/referer_parser_spec.rb' - 'spec/lib/open_food_network/user_balance_calculator_spec.rb' - 'spec/models/billable_period_spec.rb' - - 'spec/models/cart_spec.rb' - 'spec/models/product_distribution_spec.rb' + - 'spec/models/product_import/product_list_spec.rb' - 'spec/models/spree/ability_spec.rb' - 'spec/models/spree/product_spec.rb' - 'spec/models/tag_rule/filter_payment_methods_spec.rb' @@ -320,7 +327,7 @@ Layout/EmptyLinesAroundBlockBody: - 'spec/support/matchers/select2_matchers.rb' - 'spec/support/matchers/table_matchers.rb' -# Offense count: 26 +# Offense count: 24 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only @@ -330,7 +337,6 @@ Layout/EmptyLinesAroundClassBody: - 'app/controllers/admin/cache_settings_controller.rb' - 'app/controllers/admin/enterprise_fees_controller.rb' - 'app/controllers/admin/inventory_items_controller.rb' - - 'app/controllers/admin/invoice_settings_controller.rb' - 'app/controllers/admin/tag_rules_controller.rb' - 'app/controllers/api/enterprises_controller.rb' - 'app/controllers/application_controller.rb' @@ -362,7 +368,7 @@ Layout/EndAlignment: - 'app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb' - 'app/serializers/api/admin/order_cycle_serializer.rb' -# Offense count: 48 +# Offense count: 49 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. Layout/ExtraSpacing: @@ -388,6 +394,7 @@ Layout/ExtraSpacing: - 'spec/features/admin/reports_spec.rb' - 'spec/features/consumer/groups_spec.rb' - 'spec/features/consumer/shopping/shopping_spec.rb' + - 'spec/helpers/cookies_policy_helper_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - 'spec/lib/open_food_network/reports/rule_spec.rb' - 'spec/models/enterprise_fee_spec.rb' @@ -405,7 +412,7 @@ Layout/ExtraSpacing: # Offense count: 1 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: consistent, special_for_inner_method_call, special_for_inner_method_call_in_parentheses +# SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses Layout/FirstParameterIndentation: Exclude: - 'spec/controllers/spree/admin/orders/customer_details_controller_spec.rb' @@ -417,7 +424,7 @@ Layout/FirstParameterIndentation: Layout/IndentArray: EnforcedStyle: consistent -# Offense count: 51 +# Offense count: 53 # Cop supports --auto-correct. # Configuration parameters: IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces @@ -437,12 +444,11 @@ Layout/IndentationConsistency: - 'spec/models/spree/line_item_spec.rb' - 'spec/models/spree/product_spec.rb' -# Offense count: 21 +# Offense count: 20 # Cop supports --auto-correct. # Configuration parameters: Width, IgnoredPatterns. Layout/IndentationWidth: Exclude: - - 'app/controllers/admin/invoice_settings_controller.rb' - 'app/controllers/admin/order_cycles_controller.rb' - 'app/controllers/api/order_cycles_controller.rb' - 'app/models/spree/line_item_decorator.rb' @@ -460,6 +466,12 @@ Layout/IndentationWidth: - 'spec/models/enterprise_spec.rb' - 'spec/models/spree/calculator/flexi_rate_spec.rb' +# Offense count: 1 +# Cop supports --auto-correct. +Layout/LeadingBlankLines: + Exclude: + - 'lib/tasks/dev.rake' + # Offense count: 46 # Cop supports --auto-correct. Layout/LeadingCommentSpace: @@ -469,7 +481,6 @@ Layout/LeadingCommentSpace: - 'app/models/content_configuration.rb' - 'app/models/spree/inventory_unit_decorator.rb' - 'app/models/spree/taxon_decorator.rb' - - 'app/overrides/add_capture_order_shortcut.rb' - 'app/serializers/api/address_serializer.rb' - 'app/serializers/api/enterprise_serializer.rb' - 'app/serializers/api/product_serializer.rb' @@ -521,7 +532,7 @@ Layout/MultilineHashBraceLayout: - 'lib/spree/product_filters.rb' - 'spec/support/request/authentication_workflow.rb' -# Offense count: 7 +# Offense count: 6 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line @@ -529,9 +540,9 @@ Layout/MultilineMethodCallBraceLayout: Exclude: - 'app/helpers/spree/orders_helper.rb' - 'app/models/spree/variant_decorator.rb' - - 'app/overrides/add_capture_order_shortcut.rb' - 'lib/open_food_network/products_renderer.rb' - 'spec/features/admin/order_cycles_spec.rb' + - 'spec/features/consumer/shopping/orders_spec.rb' - 'spec/lib/open_food_network/products_and_inventory_report_spec.rb' # Offense count: 4 @@ -543,7 +554,7 @@ Layout/MultilineMethodCallIndentation: - 'spec/lib/open_food_network/cached_products_renderer_spec.rb' - 'spec/serializers/variant_serializer_spec.rb' -# Offense count: 30 +# Offense count: 28 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented @@ -611,7 +622,7 @@ Layout/SpaceAfterSemicolon: Exclude: - 'spec/controllers/spree/admin/base_controller_spec.rb' -# Offense count: 62 +# Offense count: 59 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: space, no_space @@ -672,11 +683,11 @@ Layout/SpaceAroundOperators: - 'lib/spree/product_filters.rb' - 'spec/controllers/admin/enterprises_controller_spec.rb' - 'spec/controllers/cart_controller_spec.rb' - - 'spec/controllers/spree/orders_controller_spec.rb' - 'spec/features/admin/bulk_order_management_spec.rb' - 'spec/features/admin/bulk_product_update_spec.rb' - 'spec/features/consumer/shopping/checkout_spec.rb' - 'spec/helpers/checkout_helper_spec.rb' + - 'spec/helpers/cookies_policy_helper_spec.rb' - 'spec/helpers/order_cycles_helper_spec.rb' - 'spec/jobs/update_billable_periods_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' @@ -713,7 +724,7 @@ Layout/SpaceInLambdaLiteral: - 'app/models/spree/product_decorator.rb' - 'app/models/spree/variant_decorator.rb' -# Offense count: 130 +# Offense count: 128 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. # SupportedStyles: space, no_space, compact @@ -728,7 +739,6 @@ Layout/SpaceInsideArrayLiteralBrackets: - 'lib/open_food_network/payments_report.rb' - 'lib/open_food_network/users_and_enterprises_report.rb' - 'spec/controllers/admin/variant_overrides_controller_spec.rb' - - 'spec/controllers/cart_controller_spec.rb' - 'spec/features/admin/reports_spec.rb' - 'spec/jobs/update_billable_periods_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' @@ -789,7 +799,7 @@ Layout/SpaceInsideBlockBraces: - 'spec/spec_helper.rb' - 'spec/support/cancan_helper.rb' -# Offense count: 772 +# Offense count: 734 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. # SupportedStyles: space, no_space, compact @@ -797,7 +807,6 @@ Layout/SpaceInsideBlockBraces: Layout/SpaceInsideHashLiteralBraces: Exclude: - 'app/controllers/admin/cache_settings_controller.rb' - - 'app/controllers/admin/contents_controller.rb' - 'app/controllers/admin/enterprise_relationships_controller.rb' - 'app/controllers/admin/enterprise_roles_controller.rb' - 'app/controllers/api/statuses_controller.rb' @@ -805,7 +814,6 @@ Layout/SpaceInsideHashLiteralBraces: - 'app/controllers/spree/admin/line_items_controller_decorator.rb' - 'app/controllers/spree/admin/products_controller_decorator.rb' - 'app/controllers/spree/admin/search_controller_decorator.rb' - - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/helpers/admin/business_model_configuration_helper.rb' - 'app/helpers/admin/injection_helper.rb' - 'app/helpers/angular_form_builder.rb' @@ -822,11 +830,9 @@ Layout/SpaceInsideHashLiteralBraces: - 'app/models/enterprise_relationship.rb' - 'app/models/producer_property.rb' - 'app/models/spree/gateway/stripe_connect.rb' - - 'app/models/spree/order_populator_decorator.rb' - 'app/models/spree/product_decorator.rb' - 'app/models/spree/property_decorator.rb' - 'app/models/spree/shipping_method_decorator.rb' - - 'app/overrides/add_capture_order_shortcut.rb' - 'app/serializers/api/admin/enterprise_fee_serializer.rb' - 'app/serializers/api/admin/order_cycle_serializer.rb' - 'lib/open_food_network/feature_toggle.rb' @@ -886,7 +892,6 @@ Layout/SpaceInsideHashLiteralBraces: - 'spec/models/spree/ability_spec.rb' - 'spec/models/spree/gateway/stripe_connect_spec.rb' - 'spec/models/spree/image_spec.rb' - - 'spec/models/spree/order_populator_spec.rb' - 'spec/models/spree/order_spec.rb' - 'spec/models/spree/product_spec.rb' - 'spec/models/spree/shipping_method_spec.rb' @@ -898,6 +903,7 @@ Layout/SpaceInsideHashLiteralBraces: - 'spec/requests/checkout/failed_checkout_spec.rb' - 'spec/requests/checkout/stripe_connect_spec.rb' - 'spec/serializers/enterprise_serializer_spec.rb' + - 'spec/services/cart_service_spec.rb' - 'spec/services/order_syncer_spec.rb' - 'spec/services/subscription_form_spec.rb' - 'spec/spec_helper.rb' @@ -926,17 +932,24 @@ Layout/SpaceInsideStringInterpolation: - 'lib/open_food_network/users_and_enterprises_report.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 5 +# Offense count: 4 # Cop supports --auto-correct. # Configuration parameters: IndentationWidth. Layout/Tab: Exclude: - - 'app/controllers/admin/invoice_settings_controller.rb' - 'app/models/spree/line_item_decorator.rb' - 'spec/lib/spree/product_filters_spec.rb' - 'spec/models/spree/line_item_spec.rb' -# Offense count: 60 +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: final_newline, final_blank_line +Layout/TrailingBlankLines: + Exclude: + - 'spec/controllers/cart_controller_spec.rb' + +# Offense count: 64 # Cop supports --auto-correct. # Configuration parameters: AllowInHeredoc. Layout/TrailingWhitespace: @@ -952,8 +965,10 @@ Layout/TrailingWhitespace: - 'app/views/json/_producer.rabl' - 'app/views/json/partials/_producer.rabl' - 'spec/controllers/admin/column_preferences_controller_spec.rb' - - 'spec/features/admin/enterprise_user_spec.rb' + - 'spec/features/admin/customers_spec.rb' - 'spec/features/admin/variant_overrides_spec.rb' + - 'spec/features/consumer/cookies_spec.rb' + - 'spec/helpers/cookies_policy_helper_spec.rb' - 'spec/helpers/enterprises_helper_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - 'spec/lib/open_food_network/group_buy_report_spec.rb' @@ -977,7 +992,7 @@ Lint/DuplicateMethods: - 'lib/discourse/single_sign_on.rb' - 'lib/open_food_network/subscription_summary.rb' -# Offense count: 18 +# Offense count: 16 Lint/IneffectiveAccessModifier: Exclude: - 'app/models/column_preference.rb' @@ -1035,7 +1050,7 @@ Lint/UnderscorePrefixedVariableName: Exclude: - 'spec/support/cancan_helper.rb' -# Offense count: 123 +# Offense count: 121 # Cop supports --auto-correct. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: @@ -1047,7 +1062,6 @@ Lint/UnusedBlockArgument: - 'app/models/column_preference.rb' - 'app/models/model_set.rb' - 'app/models/spree/order_decorator.rb' - - 'app/models/spree/order_populator_decorator.rb' - 'lib/open_food_network/bulk_coop_report.rb' - 'lib/open_food_network/enterprise_fee_calculator.rb' - 'lib/open_food_network/group_buy_report.rb' @@ -1086,7 +1100,7 @@ Lint/UnusedMethodArgument: - 'lib/open_food_network/paperclippable.rb' - 'lib/open_food_network/rack_request_blocker.rb' -# Offense count: 7 +# Offense count: 6 # Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. Lint/UselessAccessModifier: Exclude: @@ -1097,14 +1111,13 @@ Lint/UselessAccessModifier: - 'lib/open_food_network/reports/bulk_coop_report.rb' - 'spec/lib/open_food_network/reports/report_spec.rb' -# Offense count: 288 +# Offense count: 246 # Configuration parameters: CheckForMethodsWithNoSideEffects. Lint/Void: Exclude: - 'app/serializers/api/enterprise_serializer.rb' - 'spec/archive/features/consumer/checkout_spec.rb' - 'spec/controllers/api/order_cycles_controller_spec.rb' - - 'spec/controllers/cart_controller_spec.rb' - 'spec/controllers/checkout_controller_spec.rb' - 'spec/controllers/enterprises_controller_spec.rb' - 'spec/controllers/shop_controller_spec.rb' @@ -1113,10 +1126,8 @@ Lint/Void: - 'spec/controllers/spree/admin/variants_controller_spec.rb' - 'spec/controllers/spree/api/products_controller_spec.rb' - 'spec/controllers/spree/api/variants_controller_spec.rb' - - 'spec/controllers/spree/orders_controller_spec.rb' - 'spec/controllers/user_registrations_controller_spec.rb' - 'spec/features/admin/bulk_product_update_spec.rb' - - 'spec/features/admin/enterprise_fees_spec.rb' - 'spec/features/admin/enterprise_groups_spec.rb' - 'spec/features/admin/enterprises/index_spec.rb' - 'spec/features/admin/enterprises_spec.rb' @@ -1141,14 +1152,12 @@ Lint/Void: - 'spec/lib/open_food_network/reports/report_spec.rb' - 'spec/lib/open_food_network/reports/rule_spec.rb' - 'spec/mailers/order_mailer_spec.rb' - - 'spec/models/cart_spec.rb' - 'spec/models/enterprise_relationship_spec.rb' - 'spec/models/enterprise_spec.rb' - 'spec/models/exchange_spec.rb' - 'spec/models/order_cycle_spec.rb' - 'spec/models/spree/adjustment_spec.rb' - 'spec/models/spree/line_item_spec.rb' - - 'spec/models/spree/order_populator_spec.rb' - 'spec/models/spree/order_spec.rb' - 'spec/models/spree/payment_method_spec.rb' - 'spec/models/spree/payment_spec.rb' @@ -1158,10 +1167,53 @@ Lint/Void: - 'spec/serializers/enterprise_serializer_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 993 +# Offense count: 195 +Metrics/AbcSize: + Max: 293 + +# Offense count: 1010 # Configuration parameters: CountComments, ExcludedMethods. Metrics/BlockLength: - Max: 776 + Max: 787 + +# Offense count: 1 +# Configuration parameters: CountBlocks. +Metrics/BlockNesting: + Max: 4 + +# Offense count: 23 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 331 + +# Offense count: 38 +Metrics/CyclomaticComplexity: + Max: 23 + +# Offense count: 6683 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 623 + +# Offense count: 163 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 95 + +# Offense count: 27 +# Configuration parameters: CountComments. +Metrics/ModuleLength: + Max: 633 + +# Offense count: 6 +# Configuration parameters: CountKeywordArgs. +Metrics/ParameterLists: + Max: 8 + +# Offense count: 30 +Metrics/PerceivedComplexity: + Max: 21 # Offense count: 7 Naming/AccessorMethodName: @@ -1176,14 +1228,6 @@ Naming/BinaryOperatorParameterName: Exclude: - 'app/models/exchange.rb' -# Offense count: 2 -# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. -# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS -Naming/FileName: - Exclude: - - 'Gemfile' - - 'Guardfile' - # Offense count: 1 # Configuration parameters: Blacklist. # Blacklist: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) @@ -1224,12 +1268,11 @@ Naming/PredicateName: - 'lib/open_food_network/packing_report.rb' - 'lib/tasks/data.rake' -# Offense count: 14 +# Offense count: 13 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: io, id, to, by, on, in, at Naming/UncommunicativeMethodParamName: Exclude: - - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/helpers/admin/injection_helper.rb' - 'app/helpers/spree/admin/base_helper_decorator.rb' - 'app/helpers/spree/base_helper_decorator.rb' @@ -1275,6 +1318,15 @@ Performance/DoubleStartEndWith: Exclude: - 'app/helpers/application_helper.rb' +# Offense count: 4 +# Cop supports --auto-correct. +Performance/InefficientHashSearch: + Exclude: + - 'app/models/spree/payment_method_decorator.rb' + - 'app/models/spree/preferences/file_configuration.rb' + - 'lib/stripe/account_connector.rb' + - 'lib/stripe/webhook_handler.rb' + # Offense count: 3 # Cop supports --auto-correct. Performance/RedundantBlockCall: @@ -1346,6 +1398,8 @@ Rails/Delegate: - 'app/serializers/api/variant_serializer.rb' # Offense count: 8 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: slashes, arguments Rails/FilePath: Exclude: - 'lib/tasks/karma.rake' @@ -1377,14 +1431,13 @@ Rails/HasAndBelongsToMany: - 'app/models/spree/line_item_decorator.rb' - 'app/models/spree/payment_method_decorator.rb' -# Offense count: 31 +# Offense count: 29 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasManyOrHasOneDependent: Exclude: - 'app/models/account_invoice.rb' - 'app/models/billable_period.rb' - - 'app/models/cart.rb' - 'app/models/customer.rb' - 'app/models/enterprise.rb' - 'app/models/order_cycle.rb' @@ -1416,6 +1469,7 @@ Rails/HttpStatus: - 'app/controllers/api/customers_controller.rb' - 'app/controllers/api/enterprises_controller.rb' - 'app/controllers/application_controller.rb' + - 'app/controllers/cart_controller.rb' - 'app/controllers/checkout_controller.rb' - 'app/controllers/enterprises_controller.rb' - 'app/controllers/line_items_controller.rb' @@ -1423,7 +1477,6 @@ Rails/HttpStatus: - 'app/controllers/spree/admin/line_items_controller_decorator.rb' - 'app/controllers/spree/admin/products_controller_decorator.rb' - 'app/controllers/spree/credit_cards_controller.rb' - - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/controllers/spree/store_controller_decorator.rb' - 'app/controllers/stripe/callbacks_controller.rb' - 'app/controllers/stripe/webhooks_controller.rb' @@ -1470,7 +1523,7 @@ Rails/ReadWriteAttribute: Exclude: - 'app/models/enterprise.rb' -# Offense count: 47 +# Offense count: 46 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/ScopeArgs: @@ -1501,6 +1554,7 @@ Rails/TimeZone: - 'lib/open_food_network/users_and_enterprises_report.rb' - 'spec/controllers/api/statuses_controller_spec.rb' - 'spec/jobs/heartbeat_job_spec.rb' + - 'spec/lib/open_food_network/products_cache_refreshment_spec.rb' - 'spec/lib/open_food_network/products_cache_spec.rb' - 'spec/models/enterprise_relationship_spec.rb' - 'spec/models/variant_override_spec.rb' @@ -1557,7 +1611,7 @@ Style/BarePercentLiterals: - 'spec/features/admin/variants_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 210 +# Offense count: 207 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: braces, no_braces, context_dependent @@ -1568,7 +1622,6 @@ Style/BracesAroundHashParameters: - 'app/controllers/checkout_controller.rb' - 'app/controllers/spree/admin/products_controller_decorator.rb' - 'app/controllers/spree/admin/search_controller_decorator.rb' - - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/helpers/admin/account_helper.rb' - 'app/helpers/admin/business_model_configuration_helper.rb' - 'app/helpers/angular_form_builder.rb' @@ -1578,7 +1631,6 @@ Style/BracesAroundHashParameters: - 'app/jobs/update_account_invoices.rb' - 'app/jobs/update_billable_periods.rb' - 'app/models/billable_period.rb' - - 'app/models/cart.rb' - 'app/models/exchange.rb' - 'app/models/spree/adjustment_decorator.rb' - 'app/models/spree/line_item_decorator.rb' @@ -1622,11 +1674,11 @@ Style/BracesAroundHashParameters: - 'spec/models/billable_period_spec.rb' - 'spec/models/product_distribution_spec.rb' - 'spec/models/spree/ability_spec.rb' - - 'spec/models/spree/order_populator_spec.rb' - 'spec/models/spree/order_spec.rb' - 'spec/models/spree/product_spec.rb' - 'spec/models/spree/taxon_spec.rb' - 'spec/serializers/admin/customer_serializer_spec.rb' + - 'spec/services/cart_service_spec.rb' - 'spec/spec_helper.rb' - 'spec/support/cancan_helper.rb' - 'spec/support/request/authentication_workflow.rb' @@ -1638,7 +1690,7 @@ Style/CaseEquality: - 'app/helpers/angular_form_helper.rb' - 'spec/models/spree/payment_spec.rb' -# Offense count: 86 +# Offense count: 85 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, EnforcedStyle. # SupportedStyles: nested, compact @@ -1648,7 +1700,6 @@ Style/ClassAndModuleChildren: - 'app/controllers/admin/accounts_and_billing_settings_controller.rb' - 'app/controllers/admin/business_model_configuration_controller.rb' - 'app/controllers/admin/cache_settings_controller.rb' - - 'app/controllers/admin/invoice_settings_controller.rb' - 'app/controllers/spree/store_controller_decorator.rb' - 'app/helpers/angular_form_helper.rb' - 'app/models/calculator/flat_percent_per_item.rb' @@ -1751,20 +1802,15 @@ Style/CommentedKeyword: Exclude: - 'app/controllers/application_controller.rb' -# Offense count: 10 +# Offense count: 4 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. # SupportedStyles: assign_to_condition, assign_inside_condition Style/ConditionalAssignment: Exclude: - 'app/controllers/checkout_controller.rb' - - 'app/controllers/spree/admin/base_controller_decorator.rb' - - 'app/controllers/spree/admin/payment_methods_controller_decorator.rb' - 'app/controllers/spree/admin/search_controller_decorator.rb' - - 'app/helpers/spree/admin/orders_helper_decorator.rb' - - 'app/models/spree/calculator/per_item_decorator.rb' - 'app/models/spree/line_item_decorator.rb' - - 'app/models/spree/payment_decorator.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' # Offense count: 2 @@ -1817,7 +1863,7 @@ Style/FormatStringToken: - 'lib/open_food_network/sales_tax_report.rb' - 'spec/models/enterprise_spec.rb' -# Offense count: 83 +# Offense count: 79 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: @@ -1848,7 +1894,6 @@ Style/GuardClause: - 'app/models/producer_property.rb' - 'app/models/spree/classification_decorator.rb' - 'app/models/spree/order_decorator.rb' - - 'app/models/spree/order_populator_decorator.rb' - 'app/models/spree/preference_decorator.rb' - 'app/models/spree/product_decorator.rb' - 'app/models/spree/user_decorator.rb' @@ -1866,7 +1911,7 @@ Style/GuardClause: - 'spec/support/request/distribution_helper.rb' - 'spec/support/request/shop_workflow.rb' -# Offense count: 968 +# Offense count: 930 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys @@ -1881,14 +1926,12 @@ Style/HashSyntax: - 'app/controllers/admin/tag_rules_controller.rb' - 'app/controllers/api/enterprises_controller.rb' - 'app/controllers/checkout_controller.rb' - - 'app/controllers/open_food_network/cart_controller.rb' - 'app/controllers/spree/admin/line_items_controller_decorator.rb' - 'app/controllers/spree/admin/orders_controller_decorator.rb' - 'app/controllers/spree/admin/products_controller_decorator.rb' - 'app/controllers/spree/admin/search_controller_decorator.rb' - 'app/controllers/spree/admin/shipping_methods_controller_decorator.rb' - 'app/controllers/spree/api/products_controller_decorator.rb' - - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/controllers/spree/paypal_controller_decorator.rb' - 'app/controllers/spree/store_controller_decorator.rb' - 'app/controllers/user_passwords_controller.rb' @@ -1904,7 +1947,6 @@ Style/HashSyntax: - 'app/mailers/spree/user_mailer_decorator.rb' - 'app/models/billable_period.rb' - 'app/models/calculator/flat_percent_per_item.rb' - - 'app/models/cart.rb' - 'app/models/enterprise.rb' - 'app/models/enterprise_fee.rb' - 'app/models/enterprise_group.rb' @@ -1928,12 +1970,10 @@ Style/HashSyntax: - 'app/models/spree/product_set.rb' - 'app/models/spree/taxon_decorator.rb' - 'app/models/spree/user_decorator.rb' - - 'app/overrides/add_capture_order_shortcut.rb' - 'app/overrides/add_distributor_details_js_to_product.rb' - 'app/overrides/add_distributor_details_to_product.rb' - 'app/overrides/add_distributor_to_add_to_cart_form.rb' - 'app/overrides/add_enterprise_fees_to_admin_configurations_menu.rb' - - 'app/overrides/add_orders_admin_sub_menu.rb' - 'app/overrides/add_source_to_product.rb' - 'app/overrides/remove_search_bar.rb' - 'app/overrides/remove_side_bar.rb' @@ -1976,13 +2016,11 @@ Style/HashSyntax: - 'spec/controllers/spree/api/products_controller_spec.rb' - 'spec/controllers/spree/api/variants_controller_spec.rb' - 'spec/controllers/spree/credit_cards_controller_spec.rb' - - 'spec/controllers/spree/orders_controller_spec.rb' - 'spec/controllers/spree/user_sessions_controller_spec.rb' - 'spec/controllers/user_registrations_controller_spec.rb' - 'spec/features/admin/bulk_order_management_spec.rb' - 'spec/features/admin/bulk_product_update_spec.rb' - 'spec/features/admin/customers_spec.rb' - - 'spec/features/admin/enterprise_fees_spec.rb' - 'spec/features/admin/enterprise_groups_spec.rb' - 'spec/features/admin/enterprises_spec.rb' - 'spec/features/admin/order_cycles_spec.rb' @@ -1995,6 +2033,8 @@ Style/HashSyntax: - 'spec/features/admin/subscriptions_spec.rb' - 'spec/features/admin/variant_overrides_spec.rb' - 'spec/features/consumer/account/cards_spec.rb' + - 'spec/features/consumer/cookies_spec.rb' + - 'spec/features/consumer/footer_links_spec.rb' - 'spec/features/consumer/shopping/products_spec.rb' - 'spec/features/consumer/shopping/shopping_spec.rb' - 'spec/jobs/subscription_placement_job_spec.rb' @@ -2005,7 +2045,6 @@ Style/HashSyntax: - 'spec/lib/open_food_network/tag_rule_applicator_spec.rb' - 'spec/mailers/order_mailer_spec.rb' - 'spec/models/calculator/weight_spec.rb' - - 'spec/models/cart_spec.rb' - 'spec/models/enterprise_fee_spec.rb' - 'spec/models/enterprise_spec.rb' - 'spec/models/exchange_spec.rb' @@ -2060,20 +2099,18 @@ Style/MethodCallWithoutArgsParentheses: Exclude: - 'app/controllers/spree/admin/payment_methods_controller_decorator.rb' - 'app/views/json/_groups.rabl' - - 'spec/controllers/spree/orders_controller_spec.rb' + - 'spec/controllers/cart_controller_spec.rb' - 'spec/features/consumer/registration_spec.rb' - 'spec/models/spree/payment_method_spec.rb' - 'spec/support/request/ui_component_helper.rb' -# Offense count: 13 +# Offense count: 10 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline Style/MethodDefParentheses: Exclude: - - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/helpers/enterprises_helper.rb' - - 'app/models/cart.rb' - 'app/models/spree/product_decorator.rb' - 'lib/open_food_network/distribution_change_validator.rb' - 'lib/open_food_network/feature_toggle.rb' @@ -2083,7 +2120,7 @@ Style/MethodDefParentheses: - 'spec/support/request/web_helper.rb' # Offense count: 1 -Style/MethodMissing: +Style/MissingRespondToMissing: Exclude: - 'app/helpers/application_helper.rb' @@ -2139,13 +2176,12 @@ Style/Next: Exclude: - 'lib/tasks/data.rake' -# Offense count: 7 +# Offense count: 6 # Cop supports --auto-correct. Style/NilComparison: Exclude: - 'lib/discourse/single_sign_on.rb' - 'lib/open_food_network/order_grouper.rb' - - 'spec/features/admin/enterprise_fees_spec.rb' - 'spec/features/consumer/shopping/shopping_spec.rb' - 'spec/models/order_cycle_spec.rb' - 'spec/models/spree/order_spec.rb' @@ -2196,7 +2232,7 @@ Style/OneLineConditional: # Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: AllowSafeAssignment. +# Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions. Style/ParenthesesAroundCondition: Exclude: - 'app/controllers/checkout_controller.rb' @@ -2253,7 +2289,7 @@ Style/RedundantParentheses: - 'spec/controllers/admin/enterprises_controller_spec.rb' - 'spec/features/admin/bulk_product_update_spec.rb' -# Offense count: 10 +# Offense count: 8 # Cop supports --auto-correct. # Configuration parameters: AllowMultipleReturnValues. Style/RedundantReturn: @@ -2264,7 +2300,6 @@ Style/RedundantReturn: - 'app/models/enterprise_fee.rb' - 'app/models/spree/adjustment_decorator.rb' - 'app/models/spree/classification_decorator.rb' - - 'app/models/spree/order_populator_decorator.rb' - 'app/serializers/api/admin/enterprise_serializer.rb' # Offense count: 98 @@ -2300,7 +2335,7 @@ Style/RedundantSelf: - 'lib/open_food_network/reports/report.rb' - 'lib/open_food_network/variant_and_line_item_naming.rb' -# Offense count: 12 +# Offense count: 16 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed @@ -2313,6 +2348,7 @@ Style/RegexpLiteral: - 'app/models/enterprise_group.rb' - 'app/models/spree/preference_decorator.rb' - 'lib/discourse/single_sign_on.rb' + - 'spec/mailers/subscription_mailer_spec.rb' - 'spec/models/content_configuration_spec.rb' # Offense count: 4 @@ -2406,6 +2442,14 @@ Style/UnlessElse: - 'app/models/enterprise.rb' - 'lib/open_food_network/order_grouper.rb' +# Offense count: 5 +# Cop supports --auto-correct. +Style/UnneededCondition: + Exclude: + - 'app/controllers/admin/resource_controller.rb' + - 'app/controllers/application_controller.rb' + - 'app/serializers/api/order_serializer.rb' + # Offense count: 33 # Cop supports --auto-correct. Style/UnneededInterpolation: @@ -2452,7 +2496,7 @@ Style/UnneededPercentQ: - 'spec/features/consumer/producers_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 6607 +# Offense count: 6683 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https Metrics/LineLength: diff --git a/Gemfile.lock b/Gemfile.lock index 6f2939c41d..a63804e6bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -484,6 +484,7 @@ GEM immigrant (0.3.6) activerecord (>= 3.0) ipaddress (0.8.3) + jaro_winkler (1.5.1) journey (1.0.4) jquery-migrate-rails (1.2.1) jquery-rails (2.2.2) @@ -665,7 +666,8 @@ GEM rspec-retry (0.5.6) rspec-core (> 3.3, < 3.8) rspec-support (3.7.1) - rubocop (0.55.0) + rubocop (0.57.2) + jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5) powerpack (~> 0.1) From b57c6cf9acfd3bc7fd5edbaabbceec608a7763ae Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 20 Aug 2018 15:16:18 +0100 Subject: [PATCH 107/190] Add errors when attempting to update non-updatable fields --- .../stylesheets/admin/product_import.css.scss | 113 +++++++----------- .../stylesheets/admin/variables.css.scss | 1 + .../admin/product_import_controller.rb | 1 + app/models/product_import/entry_validator.rb | 31 +++++ app/models/product_import/product_importer.rb | 7 ++ .../product_import/_entries_table.html.haml | 2 +- .../product_import/_import_review.html.haml | 8 +- config/locales/en.yml | 6 + spec/models/product_importer_spec.rb | 48 ++++++-- 9 files changed, 135 insertions(+), 82 deletions(-) diff --git a/app/assets/stylesheets/admin/product_import.css.scss b/app/assets/stylesheets/admin/product_import.css.scss index d8ad019af9..20dcc481f1 100644 --- a/app/assets/stylesheets/admin/product_import.css.scss +++ b/app/assets/stylesheets/admin/product_import.css.scss @@ -1,15 +1,22 @@ @import "variables"; -div.panel-section { +$pi-red: $warning-red; +$pi-green: lighten($spree-green, 10%); +$pi-orange: $bright-orange; +$pi-blue: lighten($spree-blue, 10%); +$pi-light-yellow: #faffaf; - .neutral { - color: #bfbfbf; +// scss-lint:disable NestingDepth + +div.panel-section { + .error { + color: $pi-red; } .warning { - color: $warning-red; + color: $bright-orange; } .success { - color: #86d83a; + color: $pi-green; } .info { color: #68b7c0; @@ -85,21 +92,29 @@ div.panel-section { td, th { white-space: nowrap; } - tr.error { - color: #c84C4c; + + tr { + &.error { + color: #c84C4c; + } + + &:hover td.invalid { + background-color: darken(#f05c51, 5%); + } + + i { + display: block; + margin-bottom: -0.2em; + font-size: 1.4em !important; + } } - tr:hover td.invalid { - background-color: #ed5135; - } - tr i { - display: block; - margin-bottom: -0.2em; - font-size: 1.4em !important; - } - td.invalid { - background-color: #f05c51; - box-shadow: inset 0px 0px 1px red; - color: white; + + td { + &.invalid { + background-color: #f05c51; + box-shadow: inset 0px 0px 1px red; + color: white; + } } } @@ -123,52 +138,6 @@ br.panels.clearfix { clear: both; } -table.import-settings { - background-color: transparent !important; - width: auto; - - tbody tr:hover td { - background-color: #f3f3f3; - } - td { - border: 0; - border-bottom: 1px solid #eee; - text-align: left; - - input { - width: 15em; - } - input[type="checkbox"] { - width: auto; - } - - } - td.description { - font-weight: bold; - padding-right: 2.5em; - } - tr:first-child td { - border-top: 0; - } - tr:last-child td { - border-bottom: 0; - } - div.select2-container { - width: 13.5em; - } - div.select2-container.select2-container-disabled { - a.select2-choice, span.select2-arrow { - background-color: #d5d5d5; - } - } - input[disabled], input:disabled { - background-color: #d5d5d5; - opacity: 1; - border-color: transparent; - color: white !important; - } -} - .panel-section.import-settings { .header-description { @@ -177,7 +146,7 @@ table.import-settings { span.header-error { font-size: 0.85em; - color: $warning-red; + color: $pi-red; } .select2-search { @@ -208,10 +177,10 @@ table.import-settings { } i.fa-check-circle { - color: #86d83a; + color: $pi-green; } i.fa-info-circle { - color: #68b7c0; + color: $pi-blue; } } @@ -234,6 +203,10 @@ form.product-import, div.post-save-results, div.import-wrapper { div.import-wrapper { + .alert-box { + margin: 0 0 1.75em; + } + .ng-hide:not(.ng-hide-animate) { // We have to use !important here to override angular's display properties // scss-lint:disable ImportantRule @@ -281,7 +254,7 @@ div.progress-bar { span.progress-track{ display: block; - background: #b7ea53; + background: lighten($pi-green, 10%); height: 100%; border-radius: 0.3em; box-shadow: inset 0 0 3px rgba(0,0,0,0.3); @@ -312,7 +285,7 @@ div.progress-bar { span.category { display: inline-block; - background-color: lighten($spree-blue, 10%); + background-color: $pi-blue; color: white; padding: 0.3em 0.6em; margin: 0 0.4em 0.5em 0; diff --git a/app/assets/stylesheets/admin/variables.css.scss b/app/assets/stylesheets/admin/variables.css.scss index f8c12e0d4d..416daec5a9 100644 --- a/app/assets/stylesheets/admin/variables.css.scss +++ b/app/assets/stylesheets/admin/variables.css.scss @@ -6,6 +6,7 @@ $spree-light-blue: #eff5fc; $warning-red: #da5354; $warning-orange: #da7f52; +$bright-orange: #ffa92e; $medium-grey: #919191; $pale-blue: #cee1f4; diff --git a/app/controllers/admin/product_import_controller.rb b/app/controllers/admin/product_import_controller.rb index 38ea79a97d..eb07ccf9f9 100644 --- a/app/controllers/admin/product_import_controller.rb +++ b/app/controllers/admin/product_import_controller.rb @@ -14,6 +14,7 @@ module Admin @filepath = save_uploaded_file(params[:file]) @importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user, params[:settings]) @original_filename = params[:file].try(:original_filename) + @non_updatable_fields = ProductImport::EntryValidator.non_updatable_fields check_file_errors @importer check_spreadsheet_has_data @importer diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 33a7932f4f..9c20e26415 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -14,6 +14,16 @@ module ProductImport @import_settings = import_settings end + def self.non_updatable_fields + { + category: :primary_taxon_id, + unit_type: :variant_unit_scale, + variant_unit_name: :variant_unit_name, + tax_category: :tax_category_id, + shipping_category: :shipping_category_id + } + end + def validate_all(entries) entries.each do |entry| supplier_validation(entry) @@ -182,6 +192,8 @@ module ProductImport return end + products.each { |product| product_field_errors(entry, product) } + products.flat_map(&:variants).each do |existing_variant| if entry_matches_existing_variant?(entry, existing_variant) && existing_variant.deleted_at.nil? return mark_as_existing_variant(entry, existing_variant) @@ -215,6 +227,25 @@ module ProductImport end end + def product_field_errors(entry, existing_product) + non_updatable_fields.each do |display_name, attribute| + next if attributes_match?(attribute, existing_product, entry) || attributes_blank?(attribute, existing_product, entry) + mark_as_invalid(entry, attribute: display_name, error: I18n.t('admin.product_import.model.not_updatable')) + end + end + + def non_updatable_fields + EntryValidator.non_updatable_fields + end + + def attributes_match?(attribute, existing_product, entry) + existing_product.send(attribute) == entry.send(attribute) + end + + def attributes_blank?(attribute, existing_product, entry) + existing_product.send(attribute).blank? && entry.send(attribute).blank? + end + def permission_by_name?(supplier_name) @editable_enterprises.key?(supplier_name) end diff --git a/app/models/product_import/product_importer.rb b/app/models/product_import/product_importer.rb index 99362b8c1b..6cabcc9e2f 100644 --- a/app/models/product_import/product_importer.rb +++ b/app/models/product_import/product_importer.rb @@ -57,6 +57,13 @@ module ProductImport @sheet ? @sheet.last_row - 1 : 0 end + def product_field_errors? + @entries.each do |entry| + return true if entry.errors.messages.values.include? [I18n.t('admin.product_import.model.not_updatable')] + end + false + end + def reset_counts # Return indexed data about existing product count, reset count, and updates count per supplier @reset_counts.each do |supplier_id, values| diff --git a/app/views/admin/product_import/_entries_table.html.haml b/app/views/admin/product_import/_entries_table.html.haml index 8ad958b1c2..6b0dcfdf6e 100644 --- a/app/views/admin/product_import/_entries_table.html.haml +++ b/app/views/admin/product_import/_entries_table.html.haml @@ -7,7 +7,7 @@ %th= heading %tr{ng: {repeat: "(line_number, entry) in (entries | entriesFilterValid:'#{entries}')"}} %td - %i{ng: {class: "{'fa fa-warning warning': (count(entry.errors) > 0), 'fa fa-check-circle success': (count(entry.errors) == 0)}"}} + %i{ng: {class: "{'fa fa-warning error': (count(entry.errors) > 0), 'fa fa-check-circle success': (count(entry.errors) == 0)}"}} %td {{line_number}} %td{ng: {repeat: "(attribute, value) in entry.attributes", class: "{'invalid': attribute_invalid(attribute, line_number)}"}} diff --git a/app/views/admin/product_import/_import_review.html.haml b/app/views/admin/product_import/_import_review.html.haml index 9015c31bb8..90d8de9577 100644 --- a/app/views/admin/product_import/_import_review.html.haml +++ b/app/views/admin/product_import/_import_review.html.haml @@ -3,6 +3,12 @@ %div{ng: {controller: 'ImportFeedbackCtrl'}} + - if @importer.product_field_errors? + .alert-box.warning + = t('.not_updatable_tip') + %em= @non_updatable_fields.keys.join(', ') + "." + = t('.fields_ignored') + %div.panel-section{ng: {controller: 'DropdownPanelsCtrl'}} %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"all"))}'}} %div.header-caret @@ -21,7 +27,7 @@ %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"invalid"))}'}} %div.header-caret %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"invalid")) == 0'}} - %div.header-icon.warning + %div.header-icon.error %i.fa.fa-warning %div.header-count %strong.invalid-count diff --git a/config/locales/en.yml b/config/locales/en.yml index b9dabc2129..342e1c80f0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -514,6 +514,7 @@ en: conditional_blank: can't be blank if unit_type is blank no_product: did not match any products in the database not_found: not found in database + not_updatable: cannot be updated on existing products via product import blank: can't be blank products_no_permission: you do not have permission to manage products for this enterprise inventory_no_permission: you do not have permission to create inventory for this producer @@ -572,6 +573,11 @@ en: inventory_to_reset: Existing inventory items will have their stock reset to zero line: Line item_line: Item line + import_review: + not_updatable_tip: "The following fields cannot be updated via bulk import for existing products:" + fields_ignored: These fields will be ignored when the imported products are saved. + entries_table: + not_updatable: This field is not updatable via bulk import on existing products save_results: final_results: Import final results products_created: Products created diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index b587ad8d76..a13e951076 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -16,22 +16,23 @@ describe ProductImport::ProductImporter do let!(:category) { create(:taxon, name: 'Vegetables') } let!(:category2) { create(:taxon, name: 'Cake') } - let!(:category3) { create(:taxon, name: 'Cereal') } + let!(:category3) { create(:taxon, name: 'Meat') } + let!(:category4) { create(:taxon, name: 'Cereal') } let!(:tax_category) { create(:tax_category) } let!(:tax_category2) { create(:tax_category) } let!(:shipping_category) { create(:shipping_category) } - let!(:product) { create(:simple_product, supplier: enterprise2, name: 'Hypothetical Cake') } + let!(:product) { create(:simple_product, supplier: enterprise2, name: 'Hypothetical Cake', primary_taxon_id: category2.id) } let!(:variant) { create(:variant, product_id: product.id, price: '8.50', on_hand: '100', unit_value: '500', display_name: 'Preexisting Banana') } - let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500') } - let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500') } - let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500') } - let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500') } - let!(:product6) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Beetroot', unit_value: '500', on_demand: true, variant_unit_scale: 1, variant_unit: 'weight') } - let!(:product7) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Tomato', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight') } + let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500', primary_taxon_id: category.id) } + let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500', primary_taxon_id: category.id) } + let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500', primary_taxon_id: category.id) } + let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500', primary_taxon_id: category.id) } + let!(:product6) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Beetroot', unit_value: '500', on_demand: true, variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id) } + let!(:product7) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Tomato', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id) } - let!(:product8) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category3.id) } - let!(:product9) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category3.id) } + let!(:product8) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category4.id) } + let!(:product9) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category4.id) } let!(:variant2) { create(:variant, product_id: product8.id, price: '4.50', on_hand: '100', unit_value: '500', display_name: 'Porridge Oats') } let!(:variant3) { create(:variant, product_id: product8.id, price: '5.50', on_hand: '100', unit_value: '500', display_name: 'Rolled Oats') } let!(:variant4) { create(:variant, product_id: product9.id, price: '6.50', on_hand: '100', unit_value: '500', display_name: 'Flaked Oats') } @@ -327,6 +328,33 @@ describe ProductImport::ProductImporter do end end + describe "updating non-updatable fields on existing products" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Beetroot", "And Another Enterprise", "Meat", "5", "3.50", "500", "g"] + csv << ["Tomato", "And Another Enterprise", "Vegetables", "6", "5.50", "500", "Kg"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {'import_into' => 'product_list'} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } + + it "does not allow updating" do + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 0 + expect(filter('invalid', entries)).to eq 2 + + @importer.all_entries.each do |entry| + expect(entry.errors.messages.values).to include [I18n.t('admin.product_import.model.not_updatable')] + end + end + end + describe "when more than one product of the same name already exists with multiple variants each" do before do csv_data = CSV.generate do |csv| From cc98cc832c76605ed53bcb342826a8d50728b105 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Sat, 1 Sep 2018 13:11:47 +0100 Subject: [PATCH 108/190] Prefer #public_send over #send --- app/models/product_import/entry_validator.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 9c20e26415..54cf742b09 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -178,7 +178,7 @@ module ProductImport return if category.blank? if index.key? category - entry.send("#{type}_category_id=", index[category]) + entry.public_send("#{type}_category_id=", index[category]) else mark_as_invalid(entry, attribute: "#{type}_category", error: I18n.t('admin.product_import.model.not_found')) end @@ -239,11 +239,11 @@ module ProductImport end def attributes_match?(attribute, existing_product, entry) - existing_product.send(attribute) == entry.send(attribute) + existing_product.public_send(attribute) == entry.public_send(attribute) end def attributes_blank?(attribute, existing_product, entry) - existing_product.send(attribute).blank? && entry.send(attribute).blank? + existing_product.public_send(attribute).blank? && entry.public_send(attribute).blank? end def permission_by_name?(supplier_name) From ebb18e93941e40c43381a94161b4fac3dbedeb7f Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Sat, 1 Sep 2018 13:33:27 +0100 Subject: [PATCH 109/190] Remove unneccesary method --- app/models/product_import/entry_validator.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 54cf742b09..ce389e43c3 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -228,16 +228,12 @@ module ProductImport end def product_field_errors(entry, existing_product) - non_updatable_fields.each do |display_name, attribute| + EntryValidator.non_updatable_fields.each do |display_name, attribute| next if attributes_match?(attribute, existing_product, entry) || attributes_blank?(attribute, existing_product, entry) mark_as_invalid(entry, attribute: display_name, error: I18n.t('admin.product_import.model.not_updatable')) end end - def non_updatable_fields - EntryValidator.non_updatable_fields - end - def attributes_match?(attribute, existing_product, entry) existing_product.public_send(attribute) == entry.public_send(attribute) end From fc68e28e4d355636f26c0b7fdc7b412b49c68838 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Sat, 1 Sep 2018 14:51:10 +0100 Subject: [PATCH 110/190] Add :description to non-updatable attributes list --- app/models/product_import/entry_validator.rb | 1 + spec/features/admin/product_import_spec.rb | 2 +- spec/models/product_importer_spec.rb | 24 ++++++++++---------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index ce389e43c3..700c1e4308 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -17,6 +17,7 @@ module ProductImport def self.non_updatable_fields { category: :primary_taxon_id, + description: :description, unit_type: :variant_unit_scale, variant_unit_name: :variant_unit_name, tax_category: :tax_category_id, diff --git a/spec/features/admin/product_import_spec.rb b/spec/features/admin/product_import_spec.rb index 5d98cadb5e..4d3480d32d 100644 --- a/spec/features/admin/product_import_spec.rb +++ b/spec/features/admin/product_import_spec.rb @@ -20,7 +20,7 @@ feature "Product Import", js: true do let!(:product) { create(:simple_product, supplier: enterprise2, name: 'Hypothetical Cake') } let!(:variant) { create(:variant, product_id: product.id, price: '8.50', on_hand: '100', unit_value: '500', display_name: 'Preexisting Banana') } - let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500') } + let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500', description: '', primary_taxon_id: category.id) } let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500') } let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500') } let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500') } diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index a13e951076..538532471e 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -22,17 +22,17 @@ describe ProductImport::ProductImporter do let!(:tax_category2) { create(:tax_category) } let!(:shipping_category) { create(:shipping_category) } - let!(:product) { create(:simple_product, supplier: enterprise2, name: 'Hypothetical Cake', primary_taxon_id: category2.id) } + let!(:product) { create(:simple_product, supplier: enterprise2, name: 'Hypothetical Cake', description: nil, primary_taxon_id: category2.id) } let!(:variant) { create(:variant, product_id: product.id, price: '8.50', on_hand: '100', unit_value: '500', display_name: 'Preexisting Banana') } - let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500', primary_taxon_id: category.id) } + let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500', primary_taxon_id: category.id, description: nil) } let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500', primary_taxon_id: category.id) } let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500', primary_taxon_id: category.id) } let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500', primary_taxon_id: category.id) } - let!(:product6) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Beetroot', unit_value: '500', on_demand: true, variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id) } - let!(:product7) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Tomato', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id) } + let!(:product6) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Beetroot', unit_value: '500', on_demand: true, variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id, description: nil) } + let!(:product7) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Tomato', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id, description: nil) } - let!(:product8) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category4.id) } - let!(:product9) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category4.id) } + let!(:product8) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', description: "", unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category4.id) } + let!(:product9) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Oats', description: "", unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category4.id) } let!(:variant2) { create(:variant, product_id: product8.id, price: '4.50', on_hand: '100', unit_value: '500', display_name: 'Porridge Oats') } let!(:variant3) { create(:variant, product_id: product8.id, price: '5.50', on_hand: '100', unit_value: '500', display_name: 'Rolled Oats') } let!(:variant4) { create(:variant, product_id: product9.id, price: '6.50', on_hand: '100', unit_value: '500', display_name: 'Flaked Oats') } @@ -358,12 +358,12 @@ describe ProductImport::ProductImporter do describe "when more than one product of the same name already exists with multiple variants each" do before do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "display_name"] - csv << ["Oats", "User Enterprise", "Cereal", "50", "3.50", "500", "g", "Rolled Oats"] # Update - csv << ["Oats", "User Enterprise", "Cereal", "80", "3.75", "500", "g", "Flaked Oats"] # Update - csv << ["Oats", "User Enterprise", "Cereal", "60", "5.50", "500", "g", "Magic Oats"] # Add - csv << ["Oats", "User Enterprise", "Cereal", "70", "8.50", "500", "g", "French Oats"] # Add - csv << ["Oats", "User Enterprise", "Cereal", "70", "8.50", "500", "g", "Scottish Oats"] # Add + csv << ["name", "supplier", "category", "description", "on_hand", "price", "units", "unit_type", "display_name"] + csv << ["Oats", "User Enterprise", "Cereal", "", "50", "3.50", "500", "g", "Rolled Oats"] # Update + csv << ["Oats", "User Enterprise", "Cereal", "", "80", "3.75", "500", "g", "Flaked Oats"] # Update + csv << ["Oats", "User Enterprise", "Cereal", "", "60", "5.50", "500", "g", "Magic Oats"] # Add + csv << ["Oats", "User Enterprise", "Cereal", "", "70", "8.50", "500", "g", "French Oats"] # Add + csv << ["Oats", "User Enterprise", "Cereal", "", "70", "8.50", "500", "g", "Scottish Oats"] # Add end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') From 0e6b0aa248501abd230316f46d574013a47f43ce Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Wed, 19 Sep 2018 23:45:00 +0100 Subject: [PATCH 111/190] Deleted sample specs and one pending spec --- spec/features/admin/payment_method_spec.rb | 1 - spec/helpers/map_helper_spec.rb | 15 --------------- spec/models/account_invoice_spec.rb | 5 ----- spec/models/distributor_shipping_method_spec.rb | 5 ----- spec/views/json/producers.json.rabl_spec.rb | 15 --------------- 5 files changed, 41 deletions(-) delete mode 100644 spec/helpers/map_helper_spec.rb delete mode 100644 spec/models/account_invoice_spec.rb delete mode 100644 spec/models/distributor_shipping_method_spec.rb delete mode 100644 spec/views/json/producers.json.rabl_spec.rb diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index 9ce7f76a83..e53af5e1bf 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -176,7 +176,6 @@ feature %q{ page.should have_selector 'td', text: 'Two', count: 1 end - pending "shows me only payment methods for the enterprise I select" do pm1 pm2 diff --git a/spec/helpers/map_helper_spec.rb b/spec/helpers/map_helper_spec.rb deleted file mode 100644 index 7c9fbfb141..0000000000 --- a/spec/helpers/map_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -# Specs in this file have access to a helper object that includes -# the MapHelper. For example: -# -# describe MapHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -describe MapHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/account_invoice_spec.rb b/spec/models/account_invoice_spec.rb deleted file mode 100644 index 0473d1d455..0000000000 --- a/spec/models/account_invoice_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe AccountInvoice do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/distributor_shipping_method_spec.rb b/spec/models/distributor_shipping_method_spec.rb deleted file mode 100644 index bd8d7d2973..0000000000 --- a/spec/models/distributor_shipping_method_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe DistributorShippingMethod do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/views/json/producers.json.rabl_spec.rb b/spec/views/json/producers.json.rabl_spec.rb deleted file mode 100644 index 2f5dd633f8..0000000000 --- a/spec/views/json/producers.json.rabl_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe 'json/_producers.json.rabl' do - let!(:producer) { create(:supplier_enterprise) } - let(:render) { Rabl.render([producer], 'json/producers', view_path: 'app/views', scope: RablHelper::FakeContext.instance) } - - pending "renders a list of producers" do - render.should have_json_type(Array).at_path '' - render.should have_json_type(Object).at_path '0' - end - - pending "renders names" do - render.should be_json_eql(producer.name.to_json).at_path '0/name' - end -end From 5763bf0518520b81139f98e7ab88eaa94f2233dd Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 20 Sep 2018 10:33:02 +1000 Subject: [PATCH 112/190] Update all translations --- config/locales/en_GB.yml | 60 +- config/locales/en_US.yml | 57 +- config/locales/es.yml | 194 +++++- config/locales/fr_CA.yml | 59 +- config/locales/it.yml | 1214 +++++++++++++++++++++++++++++++++++++- config/locales/pt.yml | 58 +- config/locales/sv.yml | 55 +- 7 files changed, 1510 insertions(+), 187 deletions(-) diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml index f6170ef7d9..14021d1398 100644 --- a/config/locales/en_GB.yml +++ b/config/locales/en_GB.yml @@ -136,6 +136,7 @@ en_GB: free_trial: "free trial" plus_tax: "plus VAT" min_bill_turnover_desc: "once turnover exceeds %{mbt_amount}" + more: "More" say_no: "No" say_yes: "Yes" then: then @@ -382,6 +383,7 @@ en_GB: main_links: Main Menu Links footer_and_external_links: Footer and External Links your_content: Your content + user_guide: User Guide enterprise_fees: index: title: Enterprise Fees @@ -766,6 +768,9 @@ en_GB: search_placeholder: Search By Name manage: Manage manage_link: Settings + producer?: "Producer?" + package: "Package" + status: "Status" new_form: owner: Owner owner_tip: The primary user responsible for this enterprise. @@ -809,6 +814,8 @@ en_GB: save_reload: Save and Reload Page coordinator_fees: add: Add coordinator fee + filters: + involving: "Involving" form: incoming: Incoming supplier: Supplier @@ -822,7 +829,6 @@ en_GB: delivery_details: Pickup / Delivery details debug_info: Debug information index: - involving: Involving schedule: Schedule schedules: Schedules adding_a_new_schedule: Adding A New Schedule @@ -1843,31 +1849,31 @@ en_GB: you_have_no_orders_yet: "You have no orders yet" running_balance: "Running balance" outstanding_balance: "Outstanding balance" - admin_entreprise_relationships: "Enterprise Permissions" - 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_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_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. Camden" - admin_entreprise_groups_contact_zipcode: "Postcode" - admin_entreprise_groups_contact_zipcode_placeholder: "eg. NW1 0AA" - admin_entreprise_groups_contact_state_id: "County" - admin_entreprise_groups_contact_country_id: "Country" - admin_entreprise_groups_web: "Web Resources" - admin_entreprise_groups_web_twitter: "eg. @openfoodnetuk" - admin_entreprise_groups_web_website_placeholder: "eg. www.truffles.co.uk" + admin_enterprise_relationships: "Enterprise Permissions" + admin_enterprise_relationships_everything: "Everything" + admin_enterprise_relationships_permits: "permits" + admin_enterprise_relationships_seach_placeholder: "Search" + admin_enterprise_relationships_button_create: "Create" + admin_enterprise_groups: "Enterprise Groups" + admin_enterprise_groups_name: "Name" + admin_enterprise_groups_owner: "Owner" + admin_enterprise_groups_on_front_page: "On front page?" + admin_enterprise_groups_enterprise: "Enterprises" + admin_enterprise_groups_data_powertip: "The primary user responsible for this group." + admin_enterprise_groups_data_powertip_logo: "This is the logo for the group" + admin_enterprise_groups_data_powertip_promo_image: "This image is displayed at the top of the Group profile" + admin_enterprise_groups_contact: "Contact" + admin_enterprise_groups_contact_phone_placeholder: "eg. 98 7654 3210" + admin_enterprise_groups_contact_address1_placeholder: "eg. 123 High Street" + admin_enterprise_groups_contact_city: "Suburb" + admin_enterprise_groups_contact_city_placeholder: "eg. Newcastle" + admin_enterprise_groups_contact_zipcode: "Postcode" + admin_enterprise_groups_contact_zipcode_placeholder: "eg. 3070" + admin_enterprise_groups_contact_state_id: "County" + admin_enterprise_groups_contact_country_id: "Country" + admin_enterprise_groups_web: "Web Resources" + admin_enterprise_groups_web_twitter: "eg. @the_prof" + admin_enterprise_groups_web_website_placeholder: "eg. www.truffles.co.uk" admin_order_cycles: "Admin Order Cycles" open: "Open" close: "Close" @@ -2003,7 +2009,7 @@ en_GB: report_payment_totals: 'Payment Totals' report_all: 'all' report_order_cycle: "Order Cycle:" - report_entreprises: "Enterprises:" + report_enterprises: "Enterprises:" report_users: "Users:" report_tax_rates: Tax rates report_tax_types: Tax types diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index cd3786c5b2..167ed6aaaa 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -364,6 +364,7 @@ en_US: group_signup_page: Group signup page footer_and_external_links: Footer and External Links your_content: Your content + user_guide: User Guide enterprise_fees: index: title: Enterprise Fees @@ -670,6 +671,9 @@ en_US: search_placeholder: Search By Name manage: Manage manage_link: Settings + producer?: "Producer?" + package: "Package" + status: "Status" new_form: owner: Owner owner_tip: The primary user responsible for this enterprise. @@ -711,6 +715,8 @@ en_US: save_reload: Save and Reload Page coordinator_fees: add: Add coordinator fee + filters: + involving: "Involving" form: incoming: Incoming supplier: Supplier @@ -724,7 +730,6 @@ en_US: delivery_details: Pickup / Delivery details debug_info: Debug information index: - involving: Involving schedule: Schedule schedules: Schedules adding_a_new_schedule: Adding A New Schedule @@ -1673,30 +1678,30 @@ en_US: you_have_no_orders_yet: "You have no orders yet" running_balance: "Running balance" outstanding_balance: "Outstanding balance" - 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_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_contact: "Contact" - admin_entreprise_groups_contact_phone_placeholder: "eg. (123)435-7891" - admin_entreprise_groups_contact_address1_placeholder: "eg. 123 High Street" - admin_entreprise_groups_contact_city: "City" - admin_entreprise_groups_contact_city_placeholder: "City or Town" - admin_entreprise_groups_contact_zipcode: "Zip Code" - admin_entreprise_groups_contact_zipcode_placeholder: "eg. 24098" - 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. @openfoodnetuk" - admin_entreprise_groups_web_website_placeholder: "eg. www.truffles.com" + admin_enterprise_relationships_everything: "Everything" + admin_enterprise_relationships_permits: "permits" + admin_enterprise_relationships_seach_placeholder: "Search" + admin_enterprise_relationships_button_create: "Create" + admin_enterprise_groups: "Enterprise Groups" + admin_enterprise_groups_name: "Name" + admin_enterprise_groups_owner: "Owner" + admin_enterprise_groups_on_front_page: "On front page?" + admin_enterprise_groups_enterprise: "Enterprises" + admin_enterprise_groups_data_powertip: "The primary user responsible for this group." + admin_enterprise_groups_data_powertip_logo: "This is the logo for the group" + admin_enterprise_groups_data_powertip_promo_image: "This image is displayed at the top of the Group profile" + admin_enterprise_groups_contact: "Contact" + admin_enterprise_groups_contact_phone_placeholder: "eg. (123)435-7891" + admin_enterprise_groups_contact_address1_placeholder: "eg. 123 High Street" + admin_enterprise_groups_contact_city: "City" + admin_enterprise_groups_contact_city_placeholder: "City or Town" + admin_enterprise_groups_contact_zipcode: "Zipcode" + admin_enterprise_groups_contact_zipcode_placeholder: "eg. 24098" + admin_enterprise_groups_contact_state_id: "State" + admin_enterprise_groups_contact_country_id: "Country" + admin_enterprise_groups_web: "Web Resources" + admin_enterprise_groups_web_twitter: "eg. @the_prof" + admin_enterprise_groups_web_website_placeholder: "eg. www.truffles.com" admin_order_cycles: "Admin Order Cycles" open: "Open" close: "Close" @@ -1831,7 +1836,7 @@ en_US: report_payment_totals: 'Payment Totals' report_all: 'all' report_order_cycle: "Order Cycle:" - report_entreprises: "Enterprises:" + report_enterprises: "Enterprises:" report_users: "Users:" report_tax_rates: Tax rates report_tax_types: Tax types diff --git a/config/locales/es.yml b/config/locales/es.yml index f4c79ff1a9..34cab89377 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -64,6 +64,9 @@ es: user_passwords: spree_user: updated_not_active: "Su contraseña ha sido restablecida, pero su correo electrónico aún no ha sido confirmado." + models: + order_cycle: + cloned_order_cycle_name: "COPIA DE %{order_cycle}" enterprise_mailer: confirmation_instructions: subject: "Confirma la dirección de correo electrónico de %{enterprise}" @@ -71,9 +74,25 @@ es: subject: "%{enterprise} está ahora en %{sitename}" invite_manager: subject: "%{enterprise} te ha invitado a ser administrador" + order_mailer: + cancel_email: + dear_customer: "Estimada consumidora," + instructions: "Tu pedido ha sido CANCELADO. Guarda esta información de cancelación para tus registros." + order_summary_canceled: "Resumen del pedido [CANCELADO]" + subject: "Cancelación del pedido" + subtotal: "Subtotal: %{subtotal}" + total: "Total del pedido: %{total}" producer_mailer: order_cycle: subject: "Informe Ciclo de Pedido para %{producer}" + shipment_mailer: + shipped_email: + dear_customer: "Estimada consumidora," + instructions: "Tu pedido ha sido enviado" + shipment_summary: "Resumen de envío" + subject: "Notificación de envío" + track_information: "Información de seguimiento: %{tracking}" + track_link: "Enlace de seguimiento: %{url}" subscription_mailer: placement_summary_email: subject: Un resumen los pedidos de suscripción recientes @@ -136,6 +155,7 @@ es: free_trial: "Prueba gratuita" plus_tax: "más Impuestos" min_bill_turnover_desc: "Una vez que el volumen de negocio supere %{mbt_amount}" + more: "Más" say_no: "No" say_yes: "Si" then: Entonces @@ -382,6 +402,7 @@ es: main_links: Enlaces al menú principal footer_and_external_links: Pie de página y enlaces externos your_content: Tu contenido + user_guide: Manual de Usuario enterprise_fees: index: title: Comisiones de la Organización @@ -472,7 +493,31 @@ es: save_all_imported?: Guardar todos los productos importados? options_and_defaults: Opciones de importación y valores predeterminados no_permission: no tienes permiso para administrar esta organización + not_found: no se pudo encontrar la organización en la base de datos + blank_supplier: algunos productos tienen un nombre de proveedor en blanco + reset_absent?: Restablecer productos ausentes + default_stock: Establecer nivel de existencias + default_tax_cat: Establecer categoría de impuestos + default_shipping_cat: Establecer categoría de envío + entries_with_errors: Los artículos contienen errores y no se importarán + products_to_create: Los productos se crearán + products_to_update: Los productos se actualizarán + inventory_to_create: Se crearán Artículos de inventario + inventory_to_update: Los artículos de inventario se actualizarán line: Línea + item_line: Línea del artículo + save_results: + final_results: Importar resultados finales + products_created: Productos creados + products_updated: Productos actualizados + inventory_created: Artículos de inventario creados + inventory_updated: Artículos de inventario actualizados + all_saved: "Todos los artículos guardados con éxito" + some_saved: "Artículos guardados con éxito" + save_errors: Guardar errores + import_again: Subir otro archivo + view_products: Ir a la página de productos + view_inventory: Ir a la página de inventario variant_overrides: loading_flash: loading_inventory: CARGANDO INVENTARIO @@ -733,20 +778,33 @@ es: search_placeholder: Buscar por Nombre manage: Gestionar manage_link: Configuración + producer?: "¿Productora?" + package: "Perfil" + status: "Estado" new_form: owner: Propietaria owner_tip: La principal usaría responsable para esta organización. i_am_producer: Soy una Productora contact_name: Nombre de Contacto edit: + editing: 'Configuración:' back_link: Volver a la lista de organizaciones new: title: Nueva Organización back_link: Volver a la lista de organizaciones + remove_logo: + remove: "Eliminar la imagen" + removed_successfully: "Logotipo eliminado con éxito" + immediate_removal_warning: "El logotipo se eliminará inmediatamente después de confirmar." + remove_promo_image: + remove: "Eliminar la imagen" + removed_successfully: "Imagen promocional eliminada con éxito" + immediate_removal_warning: "La imagen promocional se eliminará inmediatamente después de confirmar." welcome: welcome_title: Bienvenida a Open Food Network! welcome_text: Has creado correctamente un next_step: Siguiente paso + choose_starting_point: 'Selecciona tu perfil:' invite_manager: user_already_exists: "El usuario ya existe" error: "Algo salió mal" @@ -774,6 +832,9 @@ es: save_reload: Guardar y recargar la página coordinator_fees: add: Añadir comisión para el coordinador + filters: + search_by_order_cycle_name: "Buscar por nombre del Ciclo de Pedido..." + involving: "Involucrando" form: incoming: Entrante supplier: Proveedora @@ -787,7 +848,6 @@ es: delivery_details: Detalles de Recogida / Entrega debug_info: Información de Debug index: - involving: Involucrando schedule: Horario schedules: Horarios adding_a_new_schedule: Agregar un nuevo horario @@ -824,7 +884,9 @@ es: bulk_update: no_data: Hm, algo salió mal. No se encontraron datos de ciclo de pedido. date_warning: + msg: Este ciclo de pedido está vinculado a %{n}pedidos de suscripción abiertos. Cambiar esta fecha ahora no afectará ningun pedido que ya se haya realizado pero se debe evitar si es posible. ¿Estás seguro que deseas continuar? cancel: Cancelar + proceed: Proceder producer_properties: index: title: Propiedades de la Productora @@ -929,11 +991,16 @@ es: address: 2. Dirección products: 3. Agregue productos review: 4. Revisar y guardar + subscription_line_items: + this_is_an_estimate: | + Los precios mostrados son solo una estimación y se calculan en el momento en que se cambia la suscripción. + Si cambias precios o tarifas, los pedidos se actualizarán pero la suscripción seguirá mostrando los valores anteriores. details: details: Detalles invalid_error: Ups! Por favor complete todos los campos requeridos ... allowed_payment_method_types_tip: Solo se pueden usar métodos de pago en efectivo y Stripe en este momento credit_card: Tarjeta de crédito + charges_not_allowed: Los cargos no están permitidos para esta consumidora loading_flash: loading: CARGANDO SUSCRIPCIONES review: @@ -970,6 +1037,11 @@ es: stripe_connect_fail: Lo sentimos, la conexión de tu cuenta Stripe ha fallado stripe_connect_settings: resource: Configuración de Stripe Connect + api: + enterprise_logo: + destroy_attachment_does_not_exist: "El logotipo no existe" + enterprise_promo_image: + destroy_attachment_does_not_exist: "La imagen promocional no existe" checkout: already_ordered: cart: "carrito" @@ -1009,6 +1081,11 @@ es: footer_legal_tos: "Terminos y condiciones" footer_legal_visit: "Encuéntrenos en" footer_legal_text_html: "Open Food Network es una plataforma libre y de código abierto. Nuestro contenido tiene una licencia %{content_license} y nuestro código %{code_license}." + footer_data_text_with_privacy_policy_html: "Cuidamos bien tus datos. Consulta nuestra %{privacy_policy} y %{cookies_policy}" + footer_data_text_without_privacy_policy_html: "Cuidamos bien tus datos. Consulta nuestra %{cookies_policy}" + footer_data_privacy_policy: "política de privacidad" + footer_data_cookies_policy: "política de cookies" + footer_skylight_dashboard_html: Los datos de rendimiento están disponibles en %{dashboard}. shop: messages: login: "login" @@ -1017,6 +1094,7 @@ es: require_customer_login: "Esta tienda es solo para consumidores registrados." require_login_html: "Haz %{login} si ya tienes una cuenta. De lo contrario realiza un %{register}y contacta con la organización para que acepten tu solicitud." require_customer_html: "%{contact} %{enterprise} para convertirte en miembro." + card_could_not_be_updated: La tarjeta no se pudo actualizar card_could_not_be_saved: la tarjeta no se pudo guardar spree_gateway_error_flash_for_checkout: "Hubo un problema con tu información de pago: %{error}" invoice_billing_address: "Dirección de facturación:" @@ -1044,9 +1122,13 @@ es: ticket_column_unit_price: "Precio por unidad" ticket_column_total_price: "Precio total" menu_1_title: "Tiendas" + menu_1_url: "/tiendas" menu_2_title: "Mapa" + menu_2_url: "/mapa" menu_3_title: "Productoras" + menu_3_url: "/productoras" menu_4_title: "Redes" + menu_4_url: "/redes" menu_5_title: "Acerca de" menu_5_url: "http://katuma.org/" menu_6_title: "Conectar" @@ -1066,6 +1148,7 @@ es: footer_email: "Correo electrónico" footer_links_md: "Enlaces" footer_about_url: "URL acerca de" + user_guide_link: "Enlace de la Guía de usuario" name: Nombre first_name: Nombre last_name: Apellido @@ -1139,6 +1222,33 @@ es: ie_warning_firefox: Descargar Firefox ie_warning_ie: Actualizar Internet Explorer ie_warning_other: "¿No puede actualizar su navegador? Pruebe Open Food Network en su teléfono :-)" + legal: + cookies_policy: + header: "Cómo utilizamos las cookies" + desc_part_1: "Las cookies son archivos de texto muy pequeños que se almacenan en tu computadora cuando visitas algunos sitios web." + desc_part_2: "En OFN somos respetuosos con tu privacidad. Utilizamos solo las cookies que son necesarias para ofrecerte el servicio de compraventa de alimentos en línea. No vendemos ninguno de tus datos. Es posible que en el futuro te propongamos que compartas alguno de tus datos para crear nuevos servicios comunes que podrían ser útiles para el ecosistema (como los servicios logísticos para sistemas alimentarios cortos), pero aún no hemos llegado a ese punto y no lo haremos sin tu autorización :-)" + desc_part_3: "Usamos cookies principalmente para recordar quién eres si 'inicias sesión' o para poder recordar los artículos que colocas en tu carrito, incluso si no has iniciado sesión. Si continúas navegando en el sitio web sin hacer clic en \"Aceptar cookies\" suponemos que nos das tu consentimiento para almacenar las cookies que son esenciales para el funcionamiento del sitio web. ¡Aquí está la lista de cookies que usamos!" + essential_cookies: "Cookies esenciales" + essential_cookies_desc: "Las siguientes cookies son estrictamente necesarias para el funcionamiento de nuestro sitio web." + essential_cookies_note: "La mayoría de las cookies solo contienen un identificador único, pero no otros datos, por lo que su dirección de correo electrónico y contraseña, por ejemplo, nunca se incluyen ni se exponen." + cookie_domain: "Establecido por:" + cookie_session_desc: "Se usa para permitir que el sitio web recuerde a los usuarios entre las visitas a la página, por ejemplo, recordar los artículos en tu carrito." + cookie_consent_desc: "Se usa para mantener el estado del consentimiento del usuario para almacenar cookies" + cookie_remember_me_desc: "Se usa si el usuario ha solicitado que el sitio web lo recuerde. Esta cookie se elimina automáticamente después de 12 días. Si como usuario deseas que se elimine esa cookie, solo necesitas desconectarte. Si no deseas que la cookie se instale en tu computadora no debes marcar la casilla \"recordarme\" al iniciar sesión." + cookie_openstreemap_desc: "Utilizado por nuestro amigo proveedor de mapeo de código abierto (OpenStreetMap) para garantizar que no recibas demasiadas solicitudes durante un período de tiempo determinado, para evitar el abuso de sus servicios." + cookie_stripe_desc: "Datos recopilados por nuestro procesador de pagos Stripe para detectar fraudes https://stripe.com/cookies-policy/legal. No todas las tiendas usan Stripe como método de pago pero es una buena práctica evitar fraude aplicarlo a todas las páginas. Stripe probablemente crea una imagen de cuáles de nuestras páginas generalmente interactúan con su API y luego marca cualquier cosa inusual. Por lo tanto configurar la cookie Stripe tiene una función más amplia que la simple provisión de un método de pago a un usuario. Eliminarla podría afectar la seguridad del servicio en sí. Puede obtener más información acerca de Stripe y leer su política de privacidad en https://stripe.com/privacy." + statistics_cookies_desc: "Las siguientes no son estrictamente necesarias, pero ayudan a proporcionarle una mejor experiencia de usuario al permitirnos analizar el comportamiento del usuario, identificar qué funciones usa más o no, comprender los problemas de la experiencia del usuario, etc." + statistics_cookies_analytics_desc_html: "Para recopilar y analizar los datos de uso de la plataforma utilizamos Google Analytics, ya que era el servicio predeterminado conectado con Spree (el software de código abierto de comercio electrónico en el que creamos) pero nuestra visión es cambiar a Matomo (ex Piwik, herramienta analítica de código abierto que cumple con GDPR y protege tu privacidad) tan pronto como podamos." + statistics_cookies_matomo_desc_html: "Para recopilar y analizar los datos de uso de la plataforma, utilizamos Matomo (ex Piwik), una herramienta analítica de código abierto que cumple con GDPR y protege tu privacidad" + statistics_cookies_matomo_optout: "¿Deseas excluirte de Matomo Analytics? No recopilamos ningún dato personal y Matomo nos ayuda a mejorar nuestro servicio, pero respetamos tu elección :-)" + cookie_analytics_utma_desc: "Se usa para distinguir usuarios y sesiones. La cookie se crea cuando la biblioteca javascript se ejecuta y no existe ninguna cookie __utma existente. La cookie se actualiza cada vez que se envían datos a Google Analytics." + cookie_analytics_utmt_desc: "Se usa para acelerar la tasa de solicitud." + disabling_cookies_header: "Advertencia sobre la desactivación de cookies" + cookies_banner: + cookies_definition: "Las cookies son archivos de texto muy pequeños que se almacenan en tu ordenador cuando visitas algunos sitios web." + cookies_policy_link_desc: "Si desea obtener más información, consulte nuestro" + cookies_policy_link: "política de cookies" + cookies_accept_button: "Aceptar cookies" home_shop: Comprar ahora brandstory_headline: "Consume con valores." brandstory_intro: "A veces la mejor forma de arreglar el sistema es empezar uno nuevo…" @@ -1228,6 +1338,7 @@ es: email_confirmation_click_link: "Por favor haga clic en el enlace de abajo para confirmar el correo electrónico y continuar configurando su perfil." email_confirmation_link_label: "Confirmar este correo electrónico »" email_confirmation_help_html: "Después de confirmar el correo electrónico puedes acceder a tu cuenta de administración para esta organización. Visita %{link} para encontrar más información sobre las características de %{sitename} y empieza a usar tu perfil o tienda online." + email_confirmation_notice_unexpected: "Recibes este mensaje porque te has registrado en %{sitename} o has sido invitado a inscribirte por alguien que probablemente conozcas. Si no entiendes por qué estás recibiendo este correo electrónico, escribe a %{contact}." email_social: "Conecte con nosotros:" email_contact: "Envíenos un correo electrónico:" email_signoff: "Saludos," @@ -1258,6 +1369,7 @@ es: email_so_edit_true_html: "Puede realizar cambios hasta que los pedidos se cierren en %{orders_close_at}." email_so_edit_false_html: "Puede ver detalles de este pedido en cualquier momento." email_so_contact_distributor_html: "Si tiene alguna pregunta, puede contactar %{distributor} a través de %{email}." + email_so_contact_distributor_to_change_order_html: "Has creado este pedido automáticamente. Puedes realizar cambios hasta que los pedidos se cierren en %{orders_close_at} contactando %{distributor} a través de %{email}." email_so_confirmation_intro_html: "Tu pedido con %{distributor} ahora está confirmado" email_so_confirmation_explainer_html: "Este pedido fue colocado automáticamente para usted, y ahora ha sido finalizado." email_so_confirmation_details_html: "Aquí encontrará todo lo que necesita saber sobre su pedido en %{distributor} :" @@ -1519,6 +1631,7 @@ es: error_number: "debe ser un número" error_email: "debe ser una dirección de correo electrónico" error_not_found_in_database: "%{name} no se encuentra en la base de datos" + error_not_primary_producer: "%{name} no está habilitado como productora" error_no_permission_for_enterprise: "\"%{name}\": no tiene permiso para administrar productos para esta organización" item_handling_fees: "Tarifa de manejo de artículo (incluída en el total de artículos)" january: "Enero" @@ -1637,6 +1750,7 @@ es: enterprise_about_headline: "¡Seguimos!" enterprise_about_message: "Ahora vamos a profundizar en los detalles acerca de" enterprise_success: "¡Felicidades! %{enterprise} se agregó a Open Food Network " + enterprise_registration_exit_message: "Si sales de este asistente en cualquier etapa puedes continuar creando tu perfil yendo a la interfaz de administración." enterprise_description: "Descripción corta" enterprise_description_placeholder: "Una frase corta que describa tu organización" enterprise_long_desc: "Descripción larga" @@ -1744,30 +1858,31 @@ es: you_have_no_orders_yet: "No tienes pedidos todavía" running_balance: "Saldo actual" outstanding_balance: "Saldo extraordinario" - admin_entreprise_relationships_everything: "Marcar todos" - admin_entreprise_relationships_permits: "Permite" - admin_entreprise_relationships_seach_placeholder: "Buscar" - admin_entreprise_relationships_button_create: "Crear" - admin_entreprise_groups: "Redes de organizaciones" - admin_entreprise_groups_name: "Nombre" - admin_entreprise_groups_owner: "Propietaria" - admin_entreprise_groups_on_front_page: "¿En la página principal?" - admin_entreprise_groups_entreprise: "Organizaciones" - admin_entreprise_groups_data_powertip: "El principal usuario responsable de este grupo." - admin_entreprise_groups_data_powertip_logo: "Este es el logo del grupo" - admin_entreprise_groups_data_powertip_promo_image: "Esta imagen se mostrará en la cabecera del perfil del Grupo" - admin_entreprise_groups_contact: "Conacto" - admin_entreprise_groups_contact_phone_placeholder: "ej. 98 7654 3210" - admin_entreprise_groups_contact_address1_placeholder: "ej. C/Torrent de l'Olla" - admin_entreprise_groups_contact_city: "Barrio" - admin_entreprise_groups_contact_city_placeholder: "ej. Barcelona" - admin_entreprise_groups_contact_zipcode: "Código Postal" - admin_entreprise_groups_contact_zipcode_placeholder: "ej. 08025" - admin_entreprise_groups_contact_state_id: "Región" - admin_entreprise_groups_contact_country_id: "País" - admin_entreprise_groups_web: "Recursos Web" - admin_entreprise_groups_web_twitter: "ej. the_prof" - admin_entreprise_groups_web_website_placeholder: "ej. www.truffles.com" + admin_enterprise_relationships: "Permisos de la organización" + admin_enterprise_relationships_everything: "Marcar todos" + admin_enterprise_relationships_permits: "Permite" + admin_enterprise_relationships_seach_placeholder: "Buscar" + admin_enterprise_relationships_button_create: "Crear" + admin_enterprise_groups: "Redes de organizaciones" + admin_enterprise_groups_name: "Nombre" + admin_enterprise_groups_owner: "Propietaria" + admin_enterprise_groups_on_front_page: "¿En la página principal?" + admin_enterprise_groups_enterprise: "Organizaciones" + admin_enterprise_groups_data_powertip: "El principal usuario responsable de este grupo." + admin_enterprise_groups_data_powertip_logo: "Este es el logo del grupo" + admin_enterprise_groups_data_powertip_promo_image: "Esta imagen se mostrará en la cabecera del perfil del Grupo" + admin_enterprise_groups_contact: "Contacto" + admin_enterprise_groups_contact_phone_placeholder: "ej. 98 7654 3210" + admin_enterprise_groups_contact_address1_placeholder: "ej. Carrer Torrent de l'Olla" + admin_enterprise_groups_contact_city: "Barrio" + admin_enterprise_groups_contact_city_placeholder: "ej. Barcelona" + admin_enterprise_groups_contact_zipcode: "Código Postal" + admin_enterprise_groups_contact_zipcode_placeholder: "ej. 08025" + admin_enterprise_groups_contact_state_id: "Provincia" + admin_enterprise_groups_contact_country_id: "País" + admin_enterprise_groups_web: "Recursos Web" + admin_enterprise_groups_web_twitter: "ej: the_prof" + admin_enterprise_groups_web_website_placeholder: "ej. www.truffles.com" admin_order_cycles: "Ciclos de Pedidos del Admin" open: "Abierta" close: "Cerrar" @@ -1867,6 +1982,7 @@ es: edit_profile_details_etc: "Cambia tu descripción, imágenes, etc." order_cycle: "Ciclo de Pedido" order_cycles: "Ciclos de Pedidos" + enterprise_relationships: "Permisos de la organización" remove_tax: "Eliminar impuesto" enterprise_tos_link: "Enlace a los Términos del Servicio de la Organización" enterprise_tos_message: "Queremos trabajar con personas que compartan nuestros objetivos y valores. Por ello, pedimos a las nuevas organizaciones que acepten" @@ -1902,7 +2018,7 @@ es: report_payment_totals: 'Pagos Totales' report_all: 'Todo' report_order_cycle: "Ciclo de Pedido:" - report_entreprises: "Organizaciones:" + report_enterprises: "Organizaciones:" report_users: "Usuarias:" report_tax_rates: Porcentajes de los Impuestos report_tax_types: Tipos de Impuestos @@ -2121,6 +2237,7 @@ es: order_cycles_no_permission_to_coordinate_error: "Ninguna de tus organizaciones tiene permiso para coordinar un ciclo de pedido" order_cycles_no_permission_to_create_error: "No tienes permiso para crear un ciclo de pedido coordinado por esta empresa." back_to_orders_list: "Volver a la lista de pedidos" + no_orders_found: "No se encontraron pedidos" order_information: "información del pedido" date_completed: "Fecha de finalización" amount: "Cantidad" @@ -2270,6 +2387,9 @@ es: select_rule_type: "Selecciona un tipo de regla:" resend_user_email_confirmation: resend: "Reenviar" + sending: "Reenviar..." + done: "Reenvio hecho ✓" + failed: "Reenvio fallido ✗" out_of_stock: reduced_stock_available: Stock reducido disponible out_of_stock_text: > @@ -2313,7 +2433,9 @@ es: Esto establecerá el nivel de stock a cero en todos los productos para este organización no está presente en el archivo subido. order_cycles: + create_failure: "Error al crear el ciclo de pedido" update_success: 'Se ha actualizado su ciclo de pedido.' + update_failure: "Error al actualizar el ciclo de pedido" no_distributors: No hay distribuidores en este ciclo de pedido. Este ciclo de pedido no será visible para las consumidoras hasta que agregues uno. ¿Deseas continuar guardando este ciclo de pedido? ' enterprises: producer: "Productora" @@ -2335,6 +2457,9 @@ es: date: "Fecha" time: "Hora" admin: + product_properties: + index: + inherits_properties_checkbox_hint: "¿Heredar propiedades desde %{supplier}? (a menos que sea anulado arriba)" orders: invoice: issued_on: Emitido el @@ -2370,6 +2495,10 @@ es: account_id: Account ID business_name: Nombre de la Organización charges_enabled: Cargos habilitados + payments: + source_forms: + stripe: + error_saving_payment: Error al guardar el pago products: new: title: 'Nuevo producto' @@ -2390,6 +2519,7 @@ es: inherits_properties?: ¿Hereda propiedades? available_on: Disponible en av_on: "Av. En" + import_date: "Fecha de importación" products_variant: variant_has_n_overrides: "Esta variante tiene %{n} override(s)" new_variant: "Nueva variante" @@ -2402,16 +2532,24 @@ es: display_as: display_as: Mostrar como reports: + table: + select_and_search: "Selecciona filtros y haz clic en BUSCAR para acceder a tus datos." bulk_coop: bulk_coop_supplier_report: 'Bulk Co-op - Totales por Proveedor' bulk_coop_allocation: 'Bulk Co-op - Asignación' bulk_coop_packing_sheets: 'Bulk Co-op - Hojas de Empaquetado' bulk_coop_customer_payments: 'Bulk Co-op - Pagos de las Consumidoras' + users: + email_confirmation: + confirmation_pending: "La confirmación por correo electrónico está pendiente. Hemos enviado un correo electrónico de confirmación a %{address}." variants: autocomplete: producer_name: Productora general_settings: edit: + legal_settings: "Configuraciones legales" + cookies_consent_banner_toggle: "Mostrar el banner de consentimiento de cookies" + privacy_policy_url: "Vínculo con la Política de privacidad" enterprises_require_tos: "Las organizaciones deben aceptar los Términos del Servicio" footer_tos_url: "URL de términos y servicios" checkout: @@ -2518,5 +2656,9 @@ es: total: Total paid?: ¿Pagado? view: Ver + saved_cards: + delete?: ¿Borrar? + cards: + authorised_shops: Tiendas autorizadas localized_number: invalid_format: tiene un formato invalido. Por favor introduzca un numero. diff --git a/config/locales/fr_CA.yml b/config/locales/fr_CA.yml index e14cf54d3e..9e459f0fe5 100644 --- a/config/locales/fr_CA.yml +++ b/config/locales/fr_CA.yml @@ -382,6 +382,7 @@ fr_CA: group_signup_page: Page d'inscription Groupe footer_and_external_links: Pied de page et Liens Externes your_content: Votre contenu + user_guide: Guide utilisateur enterprise_fees: index: title: Marges et Commissions @@ -767,6 +768,9 @@ fr_CA: search_placeholder: Recherche par nom manage: Gérer manage_link: Paramètres + producer?: "Producteur ?" + package: "Pack" + status: "Statut" new_form: owner: Gérant owner_tip: L'utilisateur principal est l'individu qui porte la responsabilité principale de l'entreprise dans le contexte de l'utilisation d'Open Food Network. @@ -810,6 +814,8 @@ fr_CA: save_reload: Sauvegarder et rafraichir la page coordinator_fees: add: Ajouter commission coordinateur + filters: + involving: "Concernant" form: incoming: Produits entrants (pouvant être mis en vente par les hubs) supplier: Fournisseur @@ -823,7 +829,6 @@ fr_CA: delivery_details: Précisions retrait / livraison debug_info: Informations de débogage index: - involving: Concernant schedule: Rythme d'abonnement schedules: Rythmes d'abonnement adding_a_new_schedule: Ajouter un nouveau rythme d'abonnement @@ -1832,31 +1837,31 @@ fr_CA: you_have_no_orders_yet: "Vous n'avez pas encore de commande" running_balance: "Solde courant" outstanding_balance: "Solde restant" - admin_entreprise_relationships: "Permissions Inter-entreprises" - admin_entreprise_relationships_everything: "Tout" - admin_entreprise_relationships_permits: "autorise" - admin_entreprise_relationships_seach_placeholder: "Chercher" - admin_entreprise_relationships_button_create: "Créer" - admin_entreprise_groups: "Groupes d'entreprises" - admin_entreprise_groups_name: "Nom" - admin_entreprise_groups_owner: "Gérant" - admin_entreprise_groups_on_front_page: "Sur la page d'accueil?" - admin_entreprise_groups_entreprise: "Entreprises" - admin_entreprise_groups_data_powertip: "L'utilisateur principal en charge de ce groupe." - admin_entreprise_groups_data_powertip_logo: "Il s'agit du logo du groupe" - admin_entreprise_groups_data_powertip_promo_image: "Cette image est affichée en haut du profil Groupe." - admin_entreprise_groups_contact: "Contact" - admin_entreprise_groups_contact_phone_placeholder: "ex: 98 7654 3210" - admin_entreprise_groups_contact_address1_placeholder: "ex: 24 rue de la croix verte" - admin_entreprise_groups_contact_city: "Ville" - admin_entreprise_groups_contact_city_placeholder: "ex: Bordeaux" - admin_entreprise_groups_contact_zipcode: "Code postal" - admin_entreprise_groups_contact_zipcode_placeholder: "ex: 14120" - admin_entreprise_groups_contact_state_id: "Département" - admin_entreprise_groups_contact_country_id: "Pays" - admin_entreprise_groups_web: "Liens web" - admin_entreprise_groups_web_twitter: "ex: @OpenFoodNet_fr" - admin_entreprise_groups_web_website_placeholder: "ex: www.monepicerieenligne.fr" + admin_enterprise_relationships: "Permissions Inter-entreprises" + admin_enterprise_relationships_everything: "Tout" + admin_enterprise_relationships_permits: "autorise" + admin_enterprise_relationships_seach_placeholder: "Chercher" + admin_enterprise_relationships_button_create: "Créer" + admin_enterprise_groups: "Groupes d'entreprises" + admin_enterprise_groups_name: "Nom" + admin_enterprise_groups_owner: "Gérant" + admin_enterprise_groups_on_front_page: "Sur la page d'accueil?" + admin_enterprise_groups_enterprise: "Entreprises" + admin_enterprise_groups_data_powertip: "L'utilisateur principal en charge de ce groupe." + admin_enterprise_groups_data_powertip_logo: "Il s'agit du logo du groupe" + admin_enterprise_groups_data_powertip_promo_image: "Cette image est affichée en haut du profil Groupe." + admin_enterprise_groups_contact: "Contact" + admin_enterprise_groups_contact_phone_placeholder: "ex: 98 7654 3210" + admin_enterprise_groups_contact_address1_placeholder: "ex: 24 rue de la croix verte" + admin_enterprise_groups_contact_city: "Ville" + admin_enterprise_groups_contact_city_placeholder: "ex: Bordeaux" + admin_enterprise_groups_contact_zipcode: "Code postal" + admin_enterprise_groups_contact_zipcode_placeholder: "ex: 44000" + admin_enterprise_groups_contact_state_id: "Département" + admin_enterprise_groups_contact_country_id: "Pays" + admin_enterprise_groups_web: "Liens web" + admin_enterprise_groups_web_twitter: "ex: @OpenFoodNet_fr" + admin_enterprise_groups_web_website_placeholder: "ex: www.maferme.fr" admin_order_cycles: "Gérer les cycles de vente" open: "Ouvre" close: "Ferme" @@ -1992,7 +1997,7 @@ fr_CA: report_payment_totals: 'Total des paiements' report_all: 'tous' report_order_cycle: "Cycle de vente:" - report_entreprises: "Entreprises:" + report_enterprises: "Entreprises:" report_users: "Utilisateurs" report_tax_rates: Taux de taxes report_tax_types: Taux de taxes par type de produit/service diff --git a/config/locales/it.yml b/config/locales/it.yml index 24eb43f696..9d46f3e777 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1,25 +1,109 @@ it: + language_name: "Inglese" activerecord: attributes: spree/order: payment_state: Stato del pagamento shipment_state: Stato della spedizione + completed_at: Completo al + number: Numero + email: Mail consumatore + spree/payment: + amount: Quantità + order_cycle: + orders_close_at: Data chiusura errors: models: spree/user: attributes: email: taken: "Esiste già un account con questa email. Ti preghiamo di effettuare il login o impostare una nuova password." + order_cycle: + attributes: + orders_close_at: + after_orders_open_at: deve essere dopo la data di apertura + activemodel: + errors: + models: + subscription_validator: + attributes: + subscription_line_items: + at_least_one_product: "^Aggiungi almeno un prodotto" + not_available: "^%{name} non è disponibile nel programma selezionato" + ends_at: + after_begins_at: "deve essere dopo l'inizio il" + customer: + does_not_belong_to_shop: "non appartiene a %{shop}" + schedule: + not_coordinated_by_shop: "non è coordinato da %{shop}" + payment_method: + not_available_to_shop: "non è disponibile al %{shop}" + shipping_method: + not_available_to_shop: "non è disponibile al %{shop}" devise: + confirmations: + send_instructions: "Riceverai entro qualche minuto un'email con le istruzioni utili a confermare il tuo account." + failed_to_send: "C'è stato un errore nell'invio della tua mail di conferma." + resend_confirmation_email: "Re-invia mail di conferma" + confirmed: "Grazie per aver confermato la tua mail! Ora puoi effettuare il log in." + not_confirmed: "Il tuo indirizzo email non può essere confermato. Forse avevi già completato questo passaggio?" + user_registrations: + spree_user: + signed_up_but_unconfirmed: "Abbiamo inviato un link di conferma al tuo indirizzo email. Per favore apri il link per attivare il tuo account." failure: invalid: | Email o password non valida. La volta scorsa eri ospite? Forse devi creare un account o resettare la tua password. + unconfirmed: "Devi confermare il tuo account prima di continuare." + already_registered: "Questo indirizzo email è già registrato. Per favore effettua il log in per continuare, otorna indietro ed utilizza un altro indirizzo email." + user_passwords: + spree_user: + updated_not_active: "La tua password è stata resettata, ma la tua email non è ancora stata confermata." enterprise_mailer: confirmation_instructions: subject: "Per favore conferma l'indirizzo email per %{enterprise}" welcome: subject: "%{enterprise} è ora su %{sitename}" + invite_manager: + subject: "%{enterprise} ti a invitato ad essere un referente" + producer_mailer: + order_cycle: + subject: "Resoconto ciclo di richieste per %{producer}" + subscription_mailer: + placement_summary_email: + subject: Un riassunto delle gentili richieste recenti + greeting: "Ciao %{name}," + intro: "Qui sotto un riassunto delle gentili richieste che sono state confermate per %{shop}." + confirmation_summary_email: + subject: Un riassunto delle gentile richieste recentemente confermate + greeting: "Ciao %{name}," + intro: "Qui sotto un riassunto delle gentili richieste che hai appena confermato per %{shop}." + summary_overview: + total: Un totale di %{count} abbonamenti sono stati contrassegnati per l'elaborazione automatica.. + success_zero: Di questi, nessuno è stato modificato con successo + success_some: Di questi, %{count} sono stati elaborati con successo. + success_all: Tutti sono stati elaborati con successo. + issues: Qui sotto i dettagli dei problemi incontrati. + summary_detail: + no_message_provided: 'Nessun messaggio di errore ' + changes: + title: Scorte insufficienti (%{count} gentili richieste) + explainer: Queste gentili richieste sono state elaborate, ma la quantità richiesta di alcuni prodotti non è disponibile + empty: + title: Nessuna scorta (%{count} gentili richieste) + explainer: Queste gentili richieste non sono state confermate perchè alcuni prodotti richiesti non sono disponibili. + complete: + title: Già elaborate (%{count} gentili richieste) + explainer: Queste gentili richieste sono già segnate come complete, e non sono quindi state elaborate. + processing: + title: Errore incontrato (%{count} gentili richieste) + explainer: L'elaborazione automatica di questi ordini è fallita a causa di un errore. L'errore è stato segnalato dove possibile. + failed_payment: + title: Pagamento non riuscito (%{count} gentili richieste) + explainer: L'elaborazione automatica del pagamento per queste gentili richieste non è riuscito a causa di un errore. L'errore è stato segnalato dove possibile. + other: + title: Altro errore (%{count} gentili richieste) + explainer: L'elaborazione automatica di queste gentili richieste non è riuscita per una ragione sconosciuta. Questo non dovrebbe accadere, ti preghiamo di contattarci se visualizzi questo messaggio. home: "OFN" title: Open Food Network welcome_to: 'Benvenuto a' @@ -29,6 +113,7 @@ it: charges_sales_tax: Addebiti l'IVA? print_invoice: "Stampa fattura" print_ticket: "Stampa Biglietto" + select_ticket_printer: "Seleziona la stampante per i biglietti" send_invoice: "Manda fattura" resend_confirmation: "Rimanda conferma" view_order: "Vedi l'ordine" @@ -46,53 +131,112 @@ it: free_trial: "Prova gratuita" plus_tax: "più IVA" min_bill_turnover_desc: "dopo the il ricambio ha superato il %{mbt_amount}" + more: "Di più" say_no: "No" say_yes: "Sì" then: poi + ongoing: Attivo + bill_address: Indirizzo di fatturazione + ship_address: Indirizzo consegna sort_order_cycles_on_shopfront_by: "Ordina cicli d'ordine in vetrina per" + required_fields: I campi obbligatori sono contrassegnati con un asterisco select_continue: Seleziona e continua remove: Rimuovi or: o + collapse_all: Riduci tutto + expand_all: Espandi tutto + loading: Caricamento... show_more: Mostra di più show_all: Mostra tutti show_all_with_more: "Mostra tutti (ancora %{num})" cancel: Annulla edit: Modifica + clone: Duplica + distributors: Distributori + distribution: Distribuzione + bulk_order_management: Gestione richieste all'ingrosso + enterprises: Aziende + enterprise_groups: Gruppi + reports: Resoconti + variant_overrides: Inventario all: Tutti + current: Attuali available: Disponibile + dashboard: Pannello di controllo + undefined: non definito + unused: non in uso + admin_and_handling: Admin + profile: Profilo + supplier_only: Solo per i fornitori weight: Peso + volume: Volume + items: Prodotti + summary: Sommario + detailed: Dettaglio + updated: Aggiornato 'yes': "Sì" 'no': "No" + y: 'SI' + n: 'NO' blocked_cookies_alert: "Il tuo browser sembra stia bloccando i cookies necessari ad usare questa pagina del negozio. Clicca sotto per consentire i cookies e ricaricare la pagina." + allow_cookies: "Accetta i cookies" notes: Note error: Eorrore + processing_payment: Elaborazione pagamento... + show_only_unfulfilled_orders: Mostra solo le gentili richieste insoddisfatte + filter_results: Filtra i risultati + quantity: Quantità + pick_up: Ritiro + copy: Copia + actions: + create_and_add_another: "Crea e aggiungi un altro" admin: + begins_at: Inizia a + begins_on: Inizia da + customer: Consumatore date: Data email: Email + ends_at: Termina a + ends_on: Termina da name: Nome on_hand: Disponibile on_demand: A richiesta on_demand?: A richiesta? order_cycle: Ciclo d'ordine + payment: Pagamento + payment_method: Metodo di pagamento phone: Telefono price: Prezzo producer: Produttore + image: Immagine product: Prodotto quantity: Quantità + schedule: Programma + shipping: Spedizione + shipping_method: Metodo di consegna shop: Negozio sku: articolo gestito a magazzino + status_state: Stato tags: Tag variant: Variante weight: Peso + volume: Volume + items: Prodotti select_all: Seleziona tutto quick_search: Ricerca veloce clear_all: Cancella tutto start_date: "Data di inizio" end_date: "Data di fine" + form_invalid: "Modulo contenente campi mancanti o non validi" clear_filters: Cancella filtri clear: Pulisci + save: Salva cancel: Annulla back: Indietro + show_more: Mostra di più + show_n_more: Mostra %{num} di più + choose: "Scegli..." + please_select: Seleziona... columns: Colonne actions: Azioni viewing: "Vista: %{current_view_name}" @@ -102,15 +246,36 @@ it: has_one_rule: "ha una regola" has_n_rules: "ha %{num} regole" unsaved_confirm_leave: "Ci sono cambiamenti non salvati in questa pagina. Continuare senza salvare?" + unsaved_changes: "Hai modifiche non salvate" accounts_and_billing_settings: edit: admin_settings: "Impostazioni" + update_invoice: "Aggiorna fatture" + auto_update_invoices: "Auto-aggiorna le fatture all'1:00 di notte" + shopfront_settings: + embedded_shopfront_settings: "Impostazioni di vetrina incorporate" + enable_embedded_shopfronts: "Disabilita Vetrine incorporata" + number_localization: + enable_localized_number: "Usa la logica internazionale di separazione migliaia/decimali" + business_model_configuration: + edit: + business_model_configuration: "Modello di business" + business_model_configuration_tip: "Configura la tariffa mensile per i negozi per l'utilizzo di Open Food Network" + bill_calculation_settings: "Impostazioni di calcolo pagamento" + bill_calculation_settings_tip: "Modifica la tariffa mensile per l'utilizzo di OFN" cache_settings: show: - error: Eorrore + distributor: Distributore + order_cycle: Ciclo d'ordine + status: Stato + error: Errore + invoice_settings: + edit: + title: Impostazioni fatturazione stripe_connect_settings: edit: settings: "Impostazioni" + status: Stato customers: index: add_customer: "Aggiungi cliente" @@ -130,6 +295,35 @@ it: select_country: 'Seleziona il paese' select_state: 'Selezione la provincia' edit: 'Modifica' + update_address: 'Aggiorna indirizzo' + confirm_delete: 'Sei sicuro di cancellare?' + search_by_email: "Cerca per email/codice..." + guest_label: 'Check-out ospite' + destroy: + has_associated_orders: 'Cancellazione non riuscita: l''utente ha ordini associati al suo negozio' + contents: + edit: + title: Contenuto + header: Intestazione + home_page: Prima pagina + producer_signup_page: Pagina accesso produttore + hub_signup_page: Pagina accesso hub + group_signup_page: Pagina accesso gruppo + main_links: Link del menu principale + footer_and_external_links: Footer e link esterni + your_content: I tuoi contenuti + user_guide: Guida per gli utenti + enterprise_fees: + index: + title: Tariffe azienda + enterprise: Azienda + fee_type: Tipo di tariffa + name: Nome + tax_category: Categoria d'imposta + calculator: Calcolatore + enterprise_groups: + index: + new_button: Nuovo gruppo di aziende products: index: unit: Unità @@ -139,11 +333,98 @@ it: inherits_properties?: Eredita proprietà? available_on: Disponibile il av_on: "Disp. il" + import_date: Importato + upload_an_image: Carica un'immagine + product_search_keywords: Cerca prodotto per parole chiave + product_search_tip: Digita le parole che possono aiutare a trovare i tuoi prodotti nei negozi. Usa lo spazio per separare ciascuna parola chiave. + SEO_keywords: Parole chiave + seo_tip: Digita le parole che possono aiutare a trovare i tuoi prodotti nel web. Usa lo spazio per separare ciascuna parola chiave. Search: Cerca + product_distributions: "Distribuzione prodotti" + group_buy_options: "Opzioni Acquisti di gruppo" + back_to_products_list: "Indietro alla lista dei prodotti" product_import: + title: Importa prodotto + file_not_found: Documento non trovato o non disponibile + no_data: Nessun dato trovato nel foglio elettronico + confirm_reset: "Questo imposterà a zero il livello di scorta di tutti i prodotti per questa \n azienda che non sono presenti nel documento aggiornato" model: + no_file: "errore: nessun documento caricato" + could_not_process: "non è stato possibile elaborare il documento: tipo di documento non valido" + incorrect_value: valore scorretto + no_product: Non corrisponde a nessun prodotto nel database + not_found: non trovato nel database blank: non può essere lasciato vuoto + products_no_permission: non sei abilitato a gestire i prodotti per questa azienda + inventory_no_permission: non sei abilitato a creare l'inventario per questo produttore + none_saved: Nessun prodotto salvato con successo + line: Linea + index: + select_file: Seleziona un foglio di calcolo da caricare + spreadsheet: Foglio di calcolo + choose_import_type: Seleziona modalità di importazione + import_into: ipo di importo + product_list: Lista prodotti + inventories: ventari + import: Importazione + upload: Carica + csv_templates: Documenti CSV + product_list_template: Scarica modello Listino Prodotti + inventory_template: Scarica modello Inventario + category_values: Valori di categoria disponibili + product_categories: Categorie Prodotti + tax_categories: Categorie imposte + shipping_categories: Categorie Spedizioni + import: + review: Revisione + import: Importazione + save: Salva + results: Risultati + save_imported: Salva i prodotti importati + no_valid_entries: Nessun elemento valido trovato + none_to_save: Nessun elemento può essere salvato + some_invalid_entries: Il file importato contiene voci non valide + fix_before_import: Per favore, correggi questi errori e prova ad importare nuovamente il documento + save_valid?: Salva le voci valide per adesso ed elimina le altre? + no_errors: Nessun errore trovato! + save_all_imported?: Vuoi salvare tutti i prodotti importati? + options_and_defaults: Importa opzioni e impostazioni standard + no_permission: Non hai il permesso di gestire questa attività + not_found: l'azienda non è stata trovata nel database + no_name: Nessun nome + blank_supplier: Alcuni prodotti non hanno il nome del produttore + reset_absent?: Elimina i prodotti assenti + reset_absent_tip: Imposta la scorta a zero per tutti i prodotti esistenti non presenti in questo file + overwrite_all: Sovrascrivi tutto + overwrite_empty: Sovrascrivi se vuoto + default_stock: Imposta il livello di scorta + default_tax_cat: Imposta la categoria d'imposta + default_shipping_cat: Imposta la categoria di spedizione + default_available_date: Imposta data disponibile + entries_with_errors: Alcuni elementi contengono errori e non verranno importati + products_to_create: Saranno creati nuovi prodotti + products_to_update: Alcuni prodotti saranno aggiornati + inventory_to_create: Saranno creati elementi dell'inventario + inventory_to_update: Elementi dell'inventario saranno aggiornati + products_to_reset: La scorta di prodotti esistenti sarà impostata a zero + inventory_to_reset: La scorta di elementi dell'inventario esistenti sarà portata a zero + line: Riga + item_line: Riga elemento + save_results: + final_results: Importa i risultati finali + products_created: Prodotti creati + products_updated: Prodotti aggiornati + inventory_created: Elementi d'inventario creati + inventory_updated: Elementi d'inventario aggiornati + all_saved: "Tutti gli elementi sono stati salvati con successo" + some_saved: "Elementi salvati con successo" + save_errors: Salva errori + import_again: Carica un altro file + view_products: Vai alla Pagina dei Prodotti + view_inventory: Vai alla Pagina dell'Inventario variant_overrides: + loading_flash: + loading_inventory: Caricamento Inventario index: title: Inventario description: Usa questa paginaper gestire gli inventari delle tue aziende. I dettagli dei prodotti impostati qui sovrascriveranno quelli impostati sulla pagina 'Prodotti' @@ -151,6 +432,7 @@ it: inherit?: Eredita? add: Aggiungi hide: Nascondi + import_date: Importato select_a_shop: Seleziona un negozio review_now: Rivedi adesso new_products_alert_message: Ci sono %{new_product_count} nuovi prodotti disponibili da aggiungere all'inventario. @@ -163,7 +445,13 @@ it: inventory_powertip: Questo è il tuo inventario dei prodotti. Per aggiungere prodotti all'inventario, seleziona 'Nuovi prodotti' dall'elenco hidden_powertip: Questi prodotti sono stati nascosti dal tuo inventario e non potranno essere aggiunti al tuo negozio. Puoi cliccare 'Aggiungi' per aggiungere un prodotto al tuo inventario. new_powertip: Questi prodotto possono essere aggiunti al tuo inventario. Clicca 'Aggiungi' per aggiungere un prodotto al tuo inventario o 'Nascondi' per nasconderlo dalla vista. Puoi sempre cambiare idea più tardi! + controls: + back_to_my_inventory: Indietro al mio inventario orders: + index: + ship: "Spedizione" + invoice_email_sent: 'La mail con la fattura è stata inviata' + order_email_resent: 'La mail con la gentile richiesta è stata re-inviata' bulk_management: tip: "Usa questa pagina per modificare le quantità su ordini multipli. I prodotti possono anche essere rimossi del tutto dagli ordini, se necessario." shared: "Risorsa condivisa?" @@ -184,32 +472,402 @@ it: max_fulfilled_units: "Massime unità riempite" order_error: "Qualche errore deve essere risolto prima che tu possa aggiornare gli ordini.\nOgni campo con i bordi rossi contiene errori." variants_without_unit_value: "ATTENZIONE: Alcune varianti non hanno il valore dell'unità" + select_variant: "Seleziona una variante" enterprise: select_outgoing_oc_products_from: Seleziona prodotti OC in uscita da enterprises: index: + title: Aziende + new_enterprise: Nuova Azienda producer?: "Produttore?" package: Imballaggio status: Stato manage: Gestisci form: + about_us: + desc_short: Breve descrizione + desc_short_placeholder: Raccontaci la tua attività in una o due frasi + desc_long: Chi siamo + desc_long_placeholder: Racconta di te ai consumatori. Questa informazione comparirà nel tuo profilo pubblico. + business_details: + display_invoice_logo: Mostra logo nelle fatture + contact: + name: Nome + email_address: 'Pubblica Indirizzo mail ' + email_address_tip: "Questo indirizzo mail sarà visualizzato nel tuo profilo pubblico" + phone: Telefono + website: Sito web + enterprise_fees: + name: Nome + fee_type: Tipo di tariffa + manage_fees: Gestisci tariffe azienda + no_fees_yet: Non hai ancora nessuna tariffa aziendale + create_button: Creane una ora + images: + logo: Logo + promo_image_placeholder: 'Questa immagine sarà visibile in "Chi Siamo"' + promo_image_note1: 'Per favore nota:' + promo_image_note2: Ogni immagine caricata sarà ritagliata a 1200 x 260 + promo_image_note3: L'immagine promo sarà visualizzata in cima alla pagina del profilo azienda e nei pop-up. + inventory_settings: + text1: Puoi optare per la gestione dei livelli di scorta e dei prezzi attraverso il tuo + inventory: inventario + text2: > + Se stai usando lo strumento inventario, puoi selezionare se i nuovi + prodotti aggiunti dai tuoi fornitori devono essere aggiunti al tuo inventario + prima di poter essere messi in scaorta. Se non utilizzi l'inventario + per gestire i tuoi prodotti dovresti selezionare a seguente opzione + "suggerita": + preferred_product_selection_from_inventory_only_yes: I nuovi prodotti possono essere inseriti nella mia vetrina (suggerita) + preferred_product_selection_from_inventory_only_no: I nuovi prodotti devono essere aggiunti al mio inventario prima di poter essere inseriti nella mia vetrina + payment_methods: + name: Nome + applies: Applica? + manage: Gestisci metodi di pagamento + not_method_yet: Non hai ancora nessun metodo di pagamento. + create_button: Crea un nuovo metodo di pagamento + create_one_button: Crea uno ora + primary_details: + name: Nome + groups: Gruppi + groups_tip: Seleziona i gruppi o le regioni di cui sei membro. Questo aiuterà i consumatori a trovare la tua azienda. + groups_placeholder: 'Inizia a digitare per trovare i gruppi ' + primary_producer: Produttore primario? + primary_producer_tip: Seleziona "Produttore" se sei un produttore primario di cibo + producer: Produttore + any: Proprio e altrui + none: Nessuno + own: Proprio + sells: Vende + sells_tip: "Nessuno - L'azienda non vende direttamente ai consumatori.
    Proprio - L'azienda vende i propri prodotti ai consumatori.
    Proprio e altrui - L'azienda può vendere prodotti propri o di altre aziende.
    " + visible_in_search: Visibile nella ricerca? + visible_in_search_tip: Determina se questa azienda sarà visibile ai consumatori quando cercano nel sito. + visible: Visibile + not_visible: Non visibile + permalink: Permalink (nessuno spazio) + permalink_tip: "Questo permalink serve a creare l'url al tuo negozio: %{link}nome-del-tuo-negozio/negozio" + link_to_front: Link alla tua vetrina + link_to_front_tip: Un link diretto alla tua vetrina su Open Food Network + shipping_methods: + name: Nome + applies: Applica? + manage: Gestisci metodi di spedizione + create_button: Crea un nuovo metodo di consegna + create_one_button: Crea uno ora + no_method_yet: Non hai ancora un metodo di consegna + shop_preferences: + shopfront_requires_login: "Vetrina visibile pubblicamente?" + shopfront_requires_login_tip: "Scegli se i consumatori devono essere registrati per poter vedere la vetrina o se è visibile a tutti" + shopfront_requires_login_false: "Pubblica" + shopfront_requires_login_true: "Visibile solo agli utenti registrati" + recommend_require_login: "Consigliamo di richiedere la registrazione quando si dà la possibilità di modificare gli ordini." + allow_guest_orders: "Gentili richieste ospiti" + allow_guest_orders_tip: "Permetti l'acquisto come ospite o richiedi di registrarsi" + allow_guest_orders_false: "Richiedi il login per ordinare" + allow_guest_orders_true: "Permetti acquisto come ospite" + allow_order_changes: "Modifica gentili richieste" + allow_order_changes_tip: "Permetti ai consumatori di modificare le proprie gentili richieste finché il ciclo di richieste è aperto." + allow_order_changes_false: "Le gentili richieste confermate non possono essere modificate / annullate" + allow_order_changes_true: "Gli utenti possono modificare / annullare le gentili richieste mentre il ciclo di richieste è aperto" + enable_subscriptions: "Sottoscrizioni" + enable_subscriptions_tip: "Abilita la funzionalità \"sottoscrizioni\"?" + enable_subscriptions_false: "Disabilitata" + enable_subscriptions_true: "Abilitata" + shopfront_message: Messaggio vetrina + shopfront_message_placeholder: > + Una spiegazione opzionale che dettaglia il funzionamento del tuo negozio, + che sarà visualizzata sopra al listino dei prodotti nella pagina del + tuo negozio. + shopfront_closed_message: Messaggio Chiusura Vetrina + shopfront_category_ordering: Categorie disponibili in Vetrina + open_date: Data apertura + close_date: Data chiusura stripe_connect: confirm_modal: cancel: Annulla users: + email_confirmation_notice_html: "Email di conferma in sospeso. Abbiamo inviato una mail di conferma a %{email}." + resend: Invia di nuovo + owner: 'Proprietario' contact: "Contatto" + contact_tip: "Il referente che riceverà le mail dell'azienda per le gentili richieste e le notifiche. Deve essere un indirizzo mail confermato." + owner_tip: L'utente responsabile per questa azienda. + notifications: Notifiche + notifications_tip: Le notifiche riguardanti gli ordini saranno inviate a questo indirizzo mail + notifications_note: 'Nota: se inserisci un nuovo indirizzo mail, ti sarà richiesto di confermarlo prima dell''uso.' + managers: Referenti + managers_tip: Altri utenti abilitati a gestire questa azienda. + invite_manager: "Invita referente" + invite_manager_tip: "Invita un utente non registrato a registrarsi e diventare referente per questa azienda." + add_unregistered_user: "Aggiungi un utente non registrato" + email_confirmed: "Email confermata" + email_not_confirmed: "Email non confermata" actions: edit_profile: Impostazioni + properties: Proprietà + payment_methods: Metodi di pagamento + payment_methods_tip: Questa azienda non ha metodi di pagamento + shipping_methods: Metodi di spedizione + shipping_methods_tip: Questa azienda ha metodi di spedizione + enterprise_fees: Tariffe azienda + enterprise_fees_tip: Questa azienda non ha tariffe aziendali + admin_index: + name: Nome + role: Ruolo + sells: Vende + visible: Visibile? + owner: Proprietario + producer: Produttore + change_type_form: + producer_profile: Profilo produttore + connect_ofn: Connetti attraverso OFN + always_free: SEMPRE LIBERO + producer_description_text: Aggiungi i tuoi prodotti, permettendo agli hubs di inserire i tuoi prodotti nei loro negozi. + producer_shop: Negozio produttore + sell_your_produce: Vendi i tuoi prodotti + producer_shop_description_text: Vendi i tuoi prodotti direttamente ai consumatori tramite la tua propria vetrina su OFN + producer_shop_description_text2: Un Negozio produttore è solo per i tuoi prodotti. Se vuoi vendere prodotti altrui, seleziona "Hub produttore". + producer_hub: Hub produttore + producer_hub_text: Vendi prodotti tuoi e di altri + producer_hub_description_text: La tua azienda è un pilastro del nostro sistema di cibo locale. Puoi vendere i tuoi prodotti, ma anche prodotti di tuoi produttori fidati attraverso la tua vetrina di Open Food Network. + profile: Solo Profilo + get_listing: Ottieni un listino + profile_description_text: Le persone ti possono trovare e contattare su OFN. La tua azienda sarà visibile sulla mappa e potrà essere trovata nelle ricerche degli utenti. + hub_shop: Isola logistica + hub_shop_text: Vendi prodotti di altri + choose_option: Per favore seleziona una delle opzioni. + change_now: Modifica ora enterprise_user_index: + loading_enterprises: Caricamento aziende + no_enterprises_found: Nessuna azienda trovata + search_placeholder: Cerca per Nome + manage: Gestisci manage_link: Impostazioni + producer?: "Produttore?" + package: "Imballaggio" + status: "Stato" + new_form: + owner: Proprietario + owner_tip: L'utente responsabile per questa azienda. + i_am_producer: Sono un produttore + contact_name: Nome contatto + edit: + editing: 'Impostazioni:' + back_link: Indietro alla lista delle aziende + new: + title: Nuova Azienda + back_link: Indietro alla lista delle aziende + welcome: + welcome_title: Benvenuto su Open Food Network! + welcome_text: Hai creato con successo una + next_step: Prossimo passo + choose_starting_point: 'Scegli il tuo imballaggio:' + invite_manager: + user_already_exists: "L'Utente esiste già" + error: "Qualcosa è andato storto" order_cycles: + edit: + advanced_settings: Impostazioni avanzate + update_and_close: Aggiorna e chiudi + choose_products_from: 'Scegli i prodotti da:' + exchange_form: + pickup_instructions_placeholder: "Istruzioni per la consegna" + pickup_instructions_tip: Queste istruzioni saranno visibili agli utenti dopo che hanno completato una gentile richiesta + pickup_time_placeholder: "Pronto per (es. Data / Ora)" + receival_instructions_placeholder: "Istruzioni per il ritiro" + add_fee: 'Aggiungi tariffa' + selected: 'selezionato' + add_exchange_form: + add_supplier: 'Aggiungi fornitore' + add_distributor: 'Aggiungi distributore' + advanced_settings: + title: Impostazioni avanzate + choose_product_tip: Puoi decidere di restringere tutti i prodotti disponibili (sia in entrata che in uscita) ai soli prodotti nell'inventario di %{inventory}. + preferred_product_selection_from_coordinator_inventory_only_here: Solo inventario del coordinatore + preferred_product_selection_from_coordinator_inventory_only_all: Tutti i prodotti disponibili + save_reload: Salva e ricarica la pagina + coordinator_fees: + add: Aggiungi un ricarico per il coordinamento + form: + supplier: Fornitore + receival_details: Dettagli ritiro + fees: Tariffe + outgoing: In uscita + distributor: Distributore + products: Prodotti + tags: Tag + add_a_tag: Aggiungi una tag + delivery_details: Dettagli per il ritiro / consegna + debug_info: Messa a punto informazioni + index: + schedule: Programma + schedules: Programma + adding_a_new_schedule: Aggiungi un nuovo programma + updating_a_schedule: Aggiorna un programma + new_schedule: Nuovo programma + create_schedule: Crea programma + update_schedule: Aggiorna programma + delete_schedule: Annulla Programma + created_schedule: Programma creato + updated_schedule: Programma aggiornato + deleted_schedule: Programma annullato + schedule_name_placeholder: Nome programma + name_required_error: Inserisci un nome per questo programma + no_order_cycles_error: Per favore seleziona almeno un ciclo di richieste (drag and drop) + name_and_timing_form: + name: Nome + orders_open: Gentili richieste aperte a + coordinator: Referente + orders_close: Gentili richieste chiuse + row: + suppliers: fornitori + distributors: distributori + variants: varianti + simple_form: + ready_for: Pronto per + ready_for_placeholder: Data / Ora + customer_instructions: Istruzioni consumatori + customer_instructions_placeholder: Note per ritiro / consegna + products: Prodotti + fees: Tariffe + destroy_errors: + orders_present: Questo ciclo di richieste è stato selezionato da un consumatore e non può essere cancellato. Per evitare altri accessi, chiudi il ciclo. + schedule_present: Questo ciclo di richieste è connesso a un programma e non può essere cancellato. Puoi eliminare il link o cancellare il programma prima. + bulk_update: + no_data: mmm, qualcosa è andato storto. Nessun dato per ciclo di richieste trovato. date_warning: + msg: 'Questo ciclo di richieste è connesso a %{n} gentili richieste aperte. Modificare questo dato ora non modificherà le gentili richieste che sono già state confermate, ma dovrebbe essere evitato per quanto possibile. Sei sicura/o di voler procedere? ' cancel: Annulla + proceed: Procedi + producer_properties: + index: + title: Proprietà produttore + proxy_orders: + cancel: + could_not_cancel_the_order: Non è possibile annullare la gentile richiesta + resume: + could_not_resume_the_order: Non è possibile riprendere la gentile richiesta + shared: + user_guide_link: + user_guide: Guida per gli utenti + overview: + enterprises_header: + ofn_with_tip: Le aziende sono produttori e/o isole logistiche e sono le unità base dell'organizzazione di OFN + enterprises_hubs_tabs: + has_no_payment_methods: "%{enterprise} non ha metodi di pagamento" + has_no_shipping_methods: "%{enterprise} non ha metodi di consegna" + has_no_enterprise_fees: "%{enterprise} non ha tariffe aziendali" + enterprise_issues: + create_new: Crea Nuovo + resend_email: Invia di nuovo Email + has_no_payment_methods: "%{enterprise} attualmente non ha metodi di pagamento" + has_no_shipping_methods: "%{enterprise} attualmente non ha metodi di consegna" + email_confirmation: "Email di conferma in sospeso. Abbiamo inviato una mail di conferma a %{email}." + not_visible: "%{enterprise} non sono visibili e quindi non possono essere trovati nella mappa o nelle ricerche" + reports: + hidden: NASCOSTO + unitsize: Dimensione unità + total: TOTALE + total_items: TOTALE ARTICOLI + supplier_totals: Totali Ciclo di richieste fornitori + supplier_totals_by_distributor: Totali Ciclo di richieste fornitori per distributore + totals_by_supplier: Totali Ciclo di richieste fornitori per fornitore + customer_totals: Totali ciclo di richieste consumatori + all_products: Tutti i prodotti + inventory: Inventario (in mano) + mailing_list: Mailing List + addresses: Indirizzi + payment_methods: Rapporto Metodi di pagamento + delivery: Rapporto Consegne + tax_types: Tipologia tariffe + pack_by_customer: Smistato dai consumatori + pack_by_supplier: Smistato dai fornitori + orders_and_distributors: + name: Gentili richieste e distributori + description: Gentili richieste con i dettagli del distributore + payments: + name: Resoconti pagamenti + description: Rapporto per i pagamenti + orders_and_fulfillment: + name: Gentili richieste e resoconti di soddifazione + customers: + name: Consumatori + products_and_inventory: + name: Prodotti e inventario + sales_total: + name: Totale vendite + description: Totale vendite per tutti gli ordini + users_and_enterprises: + name: Utenti e aziende + description: Proprietà e status aziende + order_cycle_management: + name: Gestione Ciclo di Richieste + sales_tax: + name: Imposta di vendita + packing: + name: Resoconti smistamento/imballaggio subscriptions: + subscriptions: Abbonamenti + new: Nuovo Abbonamento + create: Crea Abbonamento + index: + please_select_a_shop: Seleziona un negozio + edit_subscription: Modifica Abbonamento + pause_subscription: Metti in pausa l'abbonamento + unpause_subscription: Riprendi abbonamento + cancel_subscription: Annulla abbonamento + setup_explanation: + just_a_few_more_steps: 'Ancora pochi passi prima di cominciare:' + enable_subscriptions: "Ativa abbonamento ad almeno uno dei tuoi negozi" + enable_subscriptions_step_1_html: 1. Vai alla pagina %{enterprises_link}, trova il tuo negozio e clicca "Gestisci" + enable_subscriptions_step_2: 2. In "Preferenze Negozio", attiva l'opzione Abbonamenti + set_up_shipping_and_payment_methods_html: 'Imposta i metodi %{shipping_link} e %{payment_link} ' + set_up_shipping_and_payment_methods_note_html: Nota con gli abbonamenti può
    essere utilizzato solo il metodo Contanti + ensure_at_least_one_customer_html: 'Assicurati che esista almeno un %{customer_link} ' + create_at_least_one_schedule: Crea almeno un programma + create_at_least_one_schedule_step_1_html: '1. Vai alla pagina %{order_cycles_link} ' + create_at_least_one_schedule_step_2: 2. Crea un Ciclo di Richieste se non l'hai già fatto + create_at_least_one_schedule_step_3: '3. Clicca su"+ Nuovo programma" e compila il modulo ' + once_you_are_done_you_can_html: Quando hai fatto, puoi %{reload_this_page_link} + reload_this_page: Ricarica questa pagina + steps: + details: 1. Dettagli base + address: 2. Indirizzo + products: 3. Aggiungi prodotti + review: 4. Controlla e salva + subscription_line_items: + this_is_an_estimate: | + I prezzi visualizzati sono solo una stima e saranno calcolati nel momento in cui l'abbonamento verrà cambiato. + Se cambi i prezzi o le tariffe, le gentili richieste saranno aggiornate, ma l'abbonamento visualizzerà ancora i valori precedenti + details: + details: Dettagli + invalid_error: Oops! Per favore compila i campi obbligatori... + allowed_payment_method_types_tip: Al momento può essere utilizzato solo il metodo di pagamento Contanti + loading_flash: + loading: CARICAMENTO ABBONAMENTI review: + details: Dettagli address: Indirizzo products: Prodotti + product_already_in_order: Questo prodotto è già stato aggiunto alla gentile richiesta. Per favore modifica direttamente la quantità. + orders: + number: Numero + confirm_edit: Sei sicura/o di voler modificare questa gentile richiesta? Facendolo potrebbe essere più difficile in futuro sincronizzare le modifiche all'abbonamento + confirm_cancel_msg: Sei sicura/o di voler eliminare questo abbonamento? Quest'azione non potrà essere annullata. + cancel_failure_msg: 'Ci dispiace, eliminazione non riuscita!' + confirm_pause_msg: Sei sicura/o di voler mettere in pausa questo abbonamento? + confirm_unpause_msg: Sei sicura/o di voler riprendere questo abbonamento? + unpause_failure_msg: 'Ci dispiace, ripresa non riuscita!' + checkout: + already_ordered: + cart: "carrello" + shops: + hubs: + show_closed_shops: "Mostra i negozi chiusi" shared: + menu: + cart: + already_ordered_products: "Già richiesto in questo ciclo di richieste" register_call: selling_on_ofn: "Interessato ad entrare in Open Food Network?" register: "Registrati qui" @@ -285,13 +943,17 @@ it: terms_of_service: "Termini di servizio" on_demand: A richiesta none: Nessuno + label_shop: "Negozio" label_shops: "Negozi" label_map: "Mappa" + label_producer: "Produttore" label_producers: "Produttori" label_groups: "Gruppi" label_about: "About" label_connect: "Connetti" label_learn: "Impara" + label_blog: "Blog" + label_support: "Aiuto" label_shopping: "Acquisto" label_login: "Login" label_logout: "Logout" @@ -418,6 +1080,20 @@ it: email_payment_not_paid: NON PAGATO email_payment_summary: Riassunto del pagamento email_payment_method: "Pagamento via:" + email_so_placement_intro_html: "Hai una nuova gentile richiesta di %{distributor}" + email_so_placement_details_html: "Ecco i dettagli della gentile richiesta per %{distributor}:" + email_so_placement_changes: "Purtroppo alcuni prodotti richiesti non sono disponibili. Le quantità originali richieste sono barrate qui sotto." + email_so_placement_explainer_html: "Questa gentile richiesta è stata elaborata automaticamente per te." + email_so_edit_true_html: "Puoi apportare modifiche finchè la gentile richiesta chiuderà, il %{orders_close_at}." + email_so_edit_false_html: "Puoi visualizzare i dettagli di questa gentile richiesta in qualsiasi momento." + email_so_contact_distributor_html: "Per qualsiasi domanda, puoi contattare %{distributor} via %{email}." + email_so_contact_distributor_to_change_order_html: "Questa gentile richiesta è stata creata automaticamente per te. Puoi modificarla fino alla data di chiusura prestabilita, il %{orders_close_at} contattando %{distributor} via %{email}." + email_so_confirmation_intro_html: "La tua gentile richiesta per %{distributor} è ora confermata" + email_so_confirmation_explainer_html: "Questo ordine p stato confermato automaticamente per te, e ora è stato ultimato." + email_so_confirmation_details_html: "Ecco tutto ciò che è necessario sapere riguardo la tua gentile richiesta da %{distributor}:" + email_so_empty_intro_html: "Abbiamo cercato di confermare una nuova gentile richiesta con %{distributor}, ma abbiamo avuto dei problemi..." + email_so_empty_explainer_html: "Purtroppo nessuno dei prodotti richiesti è disponibile, quindi nessuna gentile richiesta è stata confermata. Qui sotto le quantità originali richieste sono state barrate." + email_so_empty_details_html: "Ecco i dettagli della gentile richiesta non confermata per %{distributor}:" email_shipping_delivery_details: Dettagli della consegna email_shipping_delivery_time: "Consegna il" email_shipping_delivery_address: "Indirizzo di consegna" @@ -427,9 +1103,17 @@ it: email_special_instructions: "Tue note:" email_signup_greeting: Ciao! email_signup_welcome: "Benvenuto a %{sitename}!" + email_signup_confirmed_email: "Grazie di aver confermato la tua mail." + email_signup_shop_html: "Puoi effettuare il log in qui: %{link}." email_signup_text: "Grazie per esserti unito alla rete. Se sei un cliente, non vediamo l'ora di introdurti a molti produttori fantastici, distributori di cibo spettacolari e cibo delizioso! Se sei un produttore o un'impresa del cibo, siamo entusiasti di averti come parte della rete." email_signup_help_html: "Accettiamo volentieri tutte le tue domane e i tuoi suggerimenti: puoi usare il bottone Invia Feedback sul sito o scriverci a %{email}" - producer_mail_greeting: "Caro" + invite_email: + greeting: "Ciao!" + invited_to_manage: "Sei stata/o invitata/o a gestire %{enterprise} su %{instance}." + confirm_your_email: "Dovresti aver ricevuto o ricevere a breve un'email con un link di conferma, Non potrai accedere al profilo di %{enterprise} finché non avrai confermato la tua mail." + set_a_password: "Ti sarà richiesto di impostare una password prima di poter gestire la pagina dell'azienda." + mistakenly_sent: "Non sai perché hai ricevuto questa mail? Per favore contatta %{owner_email} per maggiori informazioni." + producer_mail_greeting: "Cara/o" producer_mail_text_before: "Adesso abbiamo tutti gli ordini dei clienti per la prossima consegna." producer_mail_order_text: "Ecco il sommario degli ordini per i tuoi prodotti:" producer_mail_delivery_instructions: "Istruzioni per la consegna o il ritiro in magazzino:" @@ -449,6 +1133,7 @@ it: enterprises_ready_for: "Pronto per" enterprises_choose: "Scegli quando vuoi il tuo ordine:" maps_open: "Aperto" + maps_closed: "Chiuso" hubs_buy: "Acquista per:" hubs_shopping_here: "Compra qui" hubs_orders_closed: "Ordini chiusi" @@ -460,10 +1145,15 @@ it: hubs_filter_by: "Filtra per" hubs_filter_type: "Tipo" hubs_filter_delivery: "Consegna" + hubs_filter_property: "Proprietà" hubs_matches: "Intendevi?" hubs_intro: Fai la spesa nella tua zona hubs_distance: Più vicino a hubs_distance_filter: "Mostrami negozi vicino a %{location}" + shop_changeable_orders_alert_html: + one: La tua gentile richiesta con %{shop} / %{order} è aperta. Puoi apportare modifiche fino a%{oc_close}. + other: Hai %{count} gentili richieste con %{shop} attualmente aperte. Puoi effettuare modifiche fino a %{oc_close}. + orders_changeable_orders_alert_html: Questa gentile richiesta è stata confermata, ma puoi effettuare modifiche fino a %{oc_close}. products_clear_all: Cancella tutto products_showing: "Mostra:" products_with: con @@ -585,6 +1275,7 @@ it: shops_signup_help: Siamo pronti ad aiutare. shops_signup_help_text: Ti serve un ritorno migliore. Ti servono nuovi compratori e partner logistici. Ti serve che la tua storia sia raccontata attraverso l'ingrosso, il dettaglio e la tavola della cucina. shops_signup_detail: Ecco il dettaglio. + orders: Gentili richieste orders_fees: Commissioni... orders_edit_title: Carrello orders_edit_headline: Il tuo carrello @@ -593,6 +1284,7 @@ it: orders_edit_checkout: Paga orders_form_empty_cart: "Carrello vuoto" orders_form_subtotal: Calcola il subtotale + orders_form_admin: Admin orders_form_total: Totale orders_oc_expired_headline: Gli ordini sono chiusi per questo ciclo di ordini orders_oc_expired_text: "Spiacenti, gli ordini per questo ciclo son chiusi %{time} da! Per favore contatta direttamente il tuo hub per sapere se accettano ordini tardivi." @@ -602,6 +1294,17 @@ it: orders_oc_expired_phone: "Telefono:" orders_show_title: Conferma dell'ordine orders_show_time: Ordine pronto + orders_show_order_number: "Gentile richiesta #%{number}" + orders_show_cancelled: Annullato + orders_show_confirmed: Confermato + orders_your_order_has_been_cancelled: "La tua gentile richiesta è stata annullata" + orders_could_not_cancel: "Ci dispiace, la gentile richiesta non ha potuto essere annullata" + orders_cannot_remove_the_final_item: "Non si può rimuovere l'articolo definitivo da una gentile richiesta, per favore annulla piuttosto la gentile richiesta." + orders_bought_items_notice: + one: "Un articolo aggiuntivo è già stato confermato per questo ciclo di richieste" + other: "%{count} articoli aggiuntivi già confermati in questo ciclo di richieste" + orders_bought_already_confirmed: "* già confermato" + orders_confirm_cancel: Sei sicura/o di voler annullare questa gentile richiesta? products_cart_distributor_choice: "Distributore per il tuo ordine:" products_cart_distributor_change: "Il tuo distributore per questo ordine sarà sostituito da %{name} se aggiungi questo prodotto al tuo carrello." products_cart_distributor_is: "Il tuo distributore per quest'ordine è %{name}." @@ -627,6 +1330,7 @@ it: failed_to_create_enterprise: "La creazione della tua azienda non è andata a buon fine." failed_to_create_enterprise_unknown: "La creazione della tua azienda non è andata a buon fine.\nPer favore assicurati che tutti i campi siano completamente compilati." failed_to_update_enterprise_unknown: "Errore nell'aggiornamento della tua impresa.\nPer favore assicurati che tutti i campi siano completamente riempiti." + enterprise_confirm_delete_message: "Verrà eliminato anche il %{product} che l'azienda fornisce. Sei sicura/o di voler continuare?" order_not_saved_yet: "Il tuo ordine non è stato ancora salvato. Dacci qualche secondo per finire!" filter_by: "Filtra per" hide_filters: "Nascondi filtri" @@ -639,6 +1343,9 @@ it: error_required: "non può essere lasciato vuoto" error_number: "deve essere un numero" error_email: "deve essere un indirizzo email" + error_not_found_in_database: "%{name} non trovato nel database" + error_not_primary_producer: "%{name} non è abilitato come produttore" + error_no_permission_for_enterprise: "\"%{name}\": non sei abilitato a gestire prodotti per questa azienda" item_handling_fees: "Contributo per il trasporto dell'articolo (incluso nel totale dell'articolo)" january: "Gennaio" february: "Febbraio" @@ -653,6 +1360,7 @@ it: november: "Novembre" december: "Dicembre" email_not_found: "Indirizzo email non trovato" + email_unconfirmed: "Devi confermare il tuo indirizzo email prima di resettare la tua password." email_required: "Devi fornire un indirizzo email" logging_in: "Aspetta un attimo, ti stiamo facendo accedere" signup_email: "La tua email" @@ -667,6 +1375,7 @@ it: password_reset_sent: "Ti abbiamo mandato una email con le istruzioni per resettare la password." reset_password: "Resetta la password" who_is_managing_enterprise: "Chi è responsabile per la gestione di %{enterprise}?" + update_and_recalculate_fees: "Aggiorna e ricalcola tariffe" registration: steps: type: @@ -678,7 +1387,46 @@ it: yes_producer_help: "I produttori fanno cose buone da mangiare e/o bere. Sei un produttore se le coltivi, le allevi, le infondi, le cucini, le fai fermentare, le mungi o le modelli." no_producer_help: "Se non sei un produttore, probabilmente sei qualcuno che vende e distribuisce cibo. potresti essere un hub, una cooperativa, un gruppo d'acquisto, un rivenditore al dettaglio o all'ingrosso, o altro." create_profile: "Crea profilo" + enterprise: + registration: + modal: + steps: + details: + title: 'Dettagli' + headline: "Iniziamo!" + enterprise: "Bene! Innanzitutto abbiamo bisogno di sapere qualcosa in più sulla tua azienda:" + producer: "Bene! Innanzitutto abbiamo bisogno di sapere qualcosa in più sulla tua azienda:" + enterprise_name_field: "Nome Azienda:" + producer_name_field: "Nome Azienda:" + producer_name_field_error: "Per favore scegli un nome univoco per la tua azienda" + address1_field: "Indirizzo rigo 1:" + address1_field_error: "Per favore inserisci un indirizzo" + address2_field: "Indirizzo rigo 2:" + suburb_field: "Comune" + suburb_field_error: "Per favore inserisci un comune" + postcode_field: "CAP:" + postcode_field_error: "CAP obbligatorio" + state_field: "Provincia:" + state_field_error: "Provincia obbligatoria" + country_field: "Nazione" + country_field_error: "Seleziona una nazione" + contact: + title: 'Contatto' + contact_field: 'Contatto principale' + contact_field_placeholder: 'Nome contatto' + contact_field_required: "E' necessario inserire un contatto principale." + email_field: 'Indirizzo email' + phone_field: 'Numero di telefono' + type: + title: 'Tipo' + about: + title: 'Descrizione' + images: + title: 'Immagini' + social: + title: 'Social' enterprise_contact: "Contatto principale" + enterprise_contact_placeholder: "Nome contatto" enterprise_contact_required: "Devi inserire un contatto principale" enterprise_email_address: "Indirizzo email" enterprise_phone: "Numero telefonico" @@ -712,6 +1460,7 @@ it: enterprise_long_desc: "Descrizione lunga" enterprise_long_desc_placeholder: "Questa è la tua opportunità per raccontare la storia della tua azienda - cosa ti rende differente e fantastico? Ti suggeriamo di mantenere la descrizione sotto i 600 caratteri o le 150 parole." enterprise_long_desc_length: "%{num} caratteri / fino a 600 raccomandati" + enterprise_limit: Limite azienda enterprise_abn: "Partita IVA" enterprise_abn_placeholder: "es. 99 123 456 789" enterprise_acn: "Codice fiscale" @@ -747,6 +1496,7 @@ it: registration_finished_thanks: "Grazie per aver riempito i dettagli per %{enterprise}." registration_finished_login: "Puoi cambiare o aggiornare la tua azienda ad ogni passo accedendo a Open Food Network e andando su Amministrazione." registration_finished_action: "Open Food Network home" + registration_contact_name: 'Nome contatto' registration_type_headline: "Ultimo passo per aggiungere %{enterprise}!" registration_type_question: "Sei un produttore?" registration_type_producer: "Sì, sono un produttore" @@ -777,6 +1527,9 @@ it: registration_detail_state_error: "Lo Stato è obbligatorio" registration_detail_country: "Stato:" registration_detail_country_error: "Seleziona un paese" + shipping_method_destroy_error: "Questo metodo di consegna non può essere annullato perchè è collegato ad una gentile richiesta: %{number}." + accounts_and_billing_task_already_running_error: "Un'attività è già in esecuzione, per favore attendi " + accounts_and_billing_start_task_notice: "Attività in coda" fees: "Commissioni" item_cost: "Costo dell'articolo" bulk: "Volume" @@ -804,50 +1557,67 @@ it: credit: "Credito" Paid: "Pagato" Ready: "Pronto" + ok: OK + not_visible: non visibile you_have_no_orders_yet: "Non hai ordini al momento" running_balance: "Bilancio corrente" outstanding_balance: "Insoluto" - admin_entreprise_relationships_everything: "Tutto" - admin_entreprise_relationships_permits: "permessi" - admin_entreprise_relationships_seach_placeholder: "Cerca" - admin_entreprise_relationships_button_create: "Crea" - admin_entreprise_groups: "Gruppi dell'azienda" - admin_entreprise_groups_name: "Nome" - admin_entreprise_groups_owner: "Proprietario" - admin_entreprise_groups_on_front_page: "Sulla vetrina?" - admin_entreprise_groups_entreprise: "Aziende" - admin_entreprise_groups_data_powertip: "L'utente principale responsabile per questo gruppo." - admin_entreprise_groups_data_powertip_logo: "Questo è il logo per il gruppo" - admin_entreprise_groups_data_powertip_promo_image: "Questa immagine è mostrata in cima al profilo del Gruppo" - admin_entreprise_groups_contact: "Contatto" - admin_entreprise_groups_contact_phone_placeholder: "p.es. 01 123 4567" - admin_entreprise_groups_contact_address1_placeholder: "p.es. Viale Mazzini 1" - admin_entreprise_groups_contact_city: "Località" - admin_entreprise_groups_contact_city_placeholder: "p.es. Sesto" - admin_entreprise_groups_contact_zipcode: "CAP:" - admin_entreprise_groups_contact_zipcode_placeholder: "p.es. 20100" - admin_entreprise_groups_contact_state_id: "Stato" - admin_entreprise_groups_contact_country_id: "Stato" - admin_entreprise_groups_web: "Risorse web" - admin_entreprise_groups_web_twitter: "p.es. @az_agr" - admin_entreprise_groups_web_website_placeholder: "p.es. www.cascina.it" + admin_enterprise_relationships: "Permessi Azienda" + admin_enterprise_relationships_everything: "Tutto" + admin_enterprise_relationships_permits: "permessi" + admin_enterprise_relationships_seach_placeholder: "Cerca" + admin_enterprise_relationships_button_create: "Crea" + admin_enterprise_groups: "Gruppi dell'azienda" + admin_enterprise_groups_name: "Nome" + admin_enterprise_groups_owner: "Proprietario" + admin_enterprise_groups_on_front_page: "Sulla vetrina?" + admin_enterprise_groups_enterprise: "Aziende" + admin_enterprise_groups_data_powertip: "L'utente principale responsabile per questo gruppo." + admin_enterprise_groups_data_powertip_logo: "Questo è il logo per il gruppo" + admin_enterprise_groups_data_powertip_promo_image: "Questa immagine è mostrata in cima al profilo del Gruppo" + admin_enterprise_groups_contact: "Contatto" + admin_enterprise_groups_contact_phone_placeholder: "p.es. 01 123 4567" + admin_enterprise_groups_contact_address1_placeholder: "p.es. Viale Mazzini 1" + admin_enterprise_groups_contact_city: "Località" + admin_enterprise_groups_contact_city_placeholder: "p.es. Sesto" + admin_enterprise_groups_contact_zipcode: "CAP" + admin_enterprise_groups_contact_zipcode_placeholder: "p.es. 20100" + admin_enterprise_groups_contact_state_id: "Stato" + admin_enterprise_groups_contact_country_id: "Stato" + admin_enterprise_groups_web: "Risorse web" + admin_enterprise_groups_web_twitter: "p.es. @az_agr" + admin_enterprise_groups_web_website_placeholder: "p.es. www.cascina.it" admin_order_cycles: "Amministrazione Cicli d'ordine" open: "Aperto" close: "Chiuso" + create: "Crea" + search: "Cerca" supplier: "Fornitore" + product_name: "Nome Prodotto" + product_description: "Descrizione prodotto" + units: "Unità di misura" coordinator: "Coordinatore" distributor: "Distributore" enterprise_fees: "Contributi dell'azienda" + process_my_order: "Elabora la mia gentile richiesta" + delivery_instructions: Istruzioni di consegna + delivery_method: Metodo di consegna fee_type: "Tipo di contributo" tax_category: "Categoria di imposta" calculator: "Calcolatore" calculator_values: "Valori del calcolatore" flat_percent_per_item: "Percentuale (per prodotto)" - new_order_cycles: "Nuovi cicli d'ordine" + flat_rate_per_item: "Tariffa fissa (per articolo)" + flat_rate_per_order: "Tariffa fissa (per gentile richiesta)" + flexible_rate: "Tariffa flessibile" + new_order_cycles: "Nuovi cicli di richieste" + new_order_cycle: "Nuovo ciclo di richieste" select_a_coordinator_for_your_order_cycle: "Seleziona un coordinatore per il tuo ciclo di ordini" + notify_producers: 'Avvisa produttori' edit_order_cycle: "Modifica il ciclo d'ordine" roles: "Ruoli" update: "Aggiorna" + delete: Annulla add_producer_property: "Aggiungi proprietà produttori" in_progress: "In elaborazione" started_at: "Iniziato alle" @@ -864,6 +1634,8 @@ it: price: "Prezzo" on_hand: "A disposizione" save_changes: "Salva Modifiche" + order_saved: "Gentile richiesta salvata" + no_products: Nessun prodotto spree_admin_overview_enterprises_header: "Le Mie Aziende" spree_admin_overview_enterprises_footer: "GESTICI MIE AZIENDE" spree_admin_enterprises_hubs_name: "Nome" @@ -879,8 +1651,19 @@ it: spree_admin_single_enterprise_alert_mail_confirmation: "Conferma l'indirizzo email per" spree_admin_single_enterprise_alert_mail_sent: "Abbiamo spedito una mail a" spree_admin_overview_action_required: "Azione richiesta" + spree_admin_overview_check_your_inbox: "Controlla le tua email per ulteriori istruzioni. Grazie!" + spree_admin_unit_value: Unità di valore + spree_admin_unit_description: Descrizione Unità + spree_admin_supplier: Fornitore + spree_admin_product_category: Categoria prodotto change_package: "Cambia pacchetto" spree_admin_single_enterprise_hint: "Suggerimento: Per permettere alle persone di trovarti, abilita la tua visibilità sotto" + spree_order_availability_error: "Il distributore o il ciclo di richiesta non possono rifornire i prodotti nel tuo carrello" + spree_order_populator_error: "Il distributore o il ciclo di richieste non hanno disponibilità di alcuni prodotti nel tuo carrello. Per favore scegline un altro." + spree_order_populator_availability_error: "Questo prodotto non è disponibile dal distributore o nel ciclo di richieste selezionati." + spree_distributors_error: "Seleziona almeno un hub" + spree_user_enterprise_limit_error: "^%{email}non può gestire altre aziende (il limite è %{enterprise_limit})." + spree_variant_product_error: deve avere almeno una variante your_profil_live: "Il tuo profilo è attivo" on_ofn_map: "sulla mappa di Open Food Network" see: "Vedi" @@ -893,8 +1676,14 @@ it: manage_products: "Gestisci prodotti" edit_profile_details: "Modifica i dettagli del profilo" edit_profile_details_etc: "Cambia la descrizione del tuo profilo, le immagini, ecc." - order_cycle: "Ciclo d'ordine" + order_cycle: "Ciclo di richieste" + order_cycles: "Cicli di richieste" + enterprise_relationships: "Permessi azienda" remove_tax: "Rimuovi tassa" + enterprise_tos_link: "link Termini di Servizio Aziende" + enterprise_tos_message: "Abbiamo bisogno di lavorare con persone che condividono i nostri scopi e i nostri valori. Per questo chiediamo alle nuove aziende di sottoscrivere i nostri" + enterprise_tos_link_text: "Termini di Servizio" + enterprise_tos_agree: "Accetto i Termini di Servizio" tax_settings: "Impostazione contributi" products_require_tax_category: "i prodotto richiedono la categoria di tassa" admin_shared_address_1: "Indirizzo" @@ -912,7 +1701,7 @@ it: shop_trial_in_progress: "Il tuo negozio di prova scade tra %{days}." report_customers_distributor: "Distributore" report_customers_supplier: "Fornitore" - report_customers_cycle: "Ciclo d'ordine" + report_customers_cycle: "Ciclo di richieste" report_customers_type: "Tipo di rapporto" report_customers_csv: "Scarica come csv" report_producers: "Produttori:" @@ -925,9 +1714,100 @@ it: report_payment_totals: 'Totale pagamenti' report_all: 'tutti' report_order_cycle: "Ciclo d'ordine:" - report_entreprises: "Aziende:" + report_enterprises: "Aziende:" report_users: "Utenti:" + report_tax_rates: Tariffe + report_tax_types: Tipo tariffe + report_header_order_cycle: Ciclo di richieste + report_header_user: Utente + report_header_email: Email + report_header_status: Stato + report_header_comments: ommenti + report_header_first_name: Nome + report_header_last_name: Cognome + report_header_phone: Telefono + report_header_suburb: Comune + report_header_address: Indirizzo + report_header_billing_address: Indirizzo di fatturazione + report_header_hub: Hub + report_header_hub_address: Indirizzo hub + report_header_hub_code: Codice hub + report_header_code: Codice + report_header_paid: Pagato? + report_header_delivery: Consegna? + report_header_shipping: Spedizione + report_header_shipping_method: Metodo di consegna + report_header_shipping_instructions: Istruzioni consegna + report_header_special_instructions: Istruzioni speciali + report_header_order_number: Numero gentile richiesta + report_header_date: Data + report_header_confirmation_date: Data conferma + report_header_tags: Tag + report_header_items: Prodotti + report_header_items_total: "Totale articoli %{currency_symbol}" + report_header_taxable_items_total: "Totale Articoli con tariffe (%{currency_symbol})" + report_header_sales_tax: "Tariffa vendite (%{currency_symbol})" + report_header_delivery_charge: "Tariffa consegna (%{currency_symbol})" + report_header_enterprise: Azienda + report_header_customer: Consumatore + report_header_customer_code: Codice cliente + report_header_product: Prodotto + report_header_product_properties: Proprietà prodotto + report_header_quantity: Quantità + report_header_max_quantity: Quantità massima + report_header_total_available: Totale disponibile + report_header_supplier: Fornitore + report_header_producer: Produttore + report_header_producer_suburb: Comune produttore + report_header_unit: Unità + report_header_cost: Costo + report_header_shipping_cost: Costo consegna + report_header_total_shipping_cost: Totale costo consegna + report_header_payment_method: Metodo di pagamento + report_header_sells: Vende + report_header_visible: Visibile + report_header_price: Prezzo + report_header_unit_size: Unità di misura + report_header_distributor: Distributore + report_header_distributor_address: Indirizzo distributore + report_header_distributor_city: Città distributore + report_header_distributor_postcode: CAP distributore + report_header_delivery_address: Indirizzo consegna + report_header_delivery_postcode: CAP consegna report_header_weight: Peso + report_header_sum_total: Somma Tolale + report_header_date_of_order: Data Richiesta + report_header_amount_owing: Quantità disponibile + report_header_amount_paid: Importo pagato + report_header_units_required: Unità richieste + report_header_remainder: Pro-memoria + report_header_order_date: Data richiesta + report_header_order_id: ID richiesta + report_header_item_name: Nome articolo + report_header_customer_name: Nome Consumatore + report_header_customer_email: Email consumatore + report_header_customer_phone: Telefono Consumatore + report_header_customer_city: Città consumatore + report_header_payment_state: Stato Pagamento + report_header_payment_type: Tipo Pagamento + report_header_item_price: "Articolo (%{currency})" + report_header_item_fees_price: "Articolo + maggiorazioni (%{currency})" + report_header_admin_handling_fees: "Admin (%{currency})" + report_header_ship_price: "Consegna (%{currency})" + report_header_pay_fee_price: "Tassa pagamento (%{currency})" + report_header_total_price: "Totale (%{currency})" + report_header_product_total_price: "Totale Prodotto (%{currency})" + report_header_shipping_total_price: "Totale consegna (%{currency})" + report_header_amount: Quantità + report_header_total_cost: "Costi totali" + report_header_total_ordered: Totale richiesto + report_header_total_max: Totale max + report_header_total_units: Unità totali + report_header_sum_max_total: "Somma totale max" + report_header_total_excl_vat: "Totale imponibile (%{currency_symbol})" + report_header_total_incl_vat: "Total IVA incl. (%{currency_symbol})" + report_header_is_producer: Produttore? + report_header_not_confirmed: Non confermato initial_invoice_number: "Numero di fattura iniziale:" invoice_date: "Data fattura:" due_date: "Scadenza:" @@ -946,6 +1826,8 @@ it: products_unsaved: "Modifiche a %{n} prodotti rimangono non salvate." is_already_manager: "è già un gestore!" no_change_to_save: "Nessuna modifica da salvare" + user_invited: "%{email} è stata/o invitata/o a gestire questa azienda" + add_manager: "Aggiungi un utente esistente" users: "Utenti" about: "About" images: "Immagini" @@ -956,27 +1838,211 @@ it: social: "Social" business_details: "Dettagli aziendali" properties: "Proprietà" + shipping: "Spedizione" shipping_methods: "Metodi di Spedizione" payment_methods: "Metodi di pagamento" payment_method_fee: "Imposta di transizione" inventory_settings: "Impostazioni dell'inventario" tag_rules: "Regole dei tag" shop_preferences: "Preferenze di acquisto" + enterprise_fee_whole_order: Ordine intero + enterprise_fee_by: "%{type} tariffa da %{role} %{enterprise_name}" validation_msg_relationship_already_established: "^Questa relazione è già stabilita." validation_msg_at_least_one_hub: "^Almeno un hub deve essere selezionato" validation_msg_product_category_cant_be_blank: "^Categoria di prodotto non può essere vuoto" validation_msg_tax_category_cant_be_blank: "^Categoria di tassa non può essere vuoto" validation_msg_is_associated_with_an_exising_customer: "è associato con un cliente esistente" + enterprise_name_error: "è già stato utilizzato. Se questo è il nome della tua azienda e vorresti reclamarne la proprietà, o se vuoi contattare questa azienda, puoi contattare l'attuale referente di questo profilo a %{email}." + enterprise_owner_error: "^%{email}non può gestire altre aziende (il limite è %{enterprise_limit})." + enterprise_role_uniqueness_error: "^Questo ruolo è già presente." + inventory_item_visibility_error: deve essere vero o falso + product_importer_file_error: "errore: nessun documento caricato" + product_importer_spreadsheet_error: "non è stato possibile elaborare il documento: tipo di documento non valido" + product_importer_products_save_error: Nessun prodotto salvato con successo + product_import_file_not_found_notice: 'Documento non trovato o non disponibile' + product_import_no_data_in_spreadsheet_notice: 'Nessun dato trovato nel foglio elettronico' + order_choosing_hub_notice: Il tuo hub è stato selezionato + order_cycle_selecting_notice: Il tuo ciclo di richieste è stato selezionato + active_distributors_not_ready_for_checkout_message_singular: >- + L'hub %{distributor_names} figura in un ciclo di richieste attivo, ma non ha + metodi di consegna e di pagamento validi. Finché non li imposti, i consumatori + non potranno acquistare da questo hub. + active_distributors_not_ready_for_checkout_message_plural: >- + Gli hub %{distributor_names}figurano in un ciclo di richieste attivo, ma non + hanno metodi di consegna e di pagamento validi. Finché non li imposti, i consumatori + non potranno acquistare da questi hub. + enterprise_fees_update_notice: Le tariffe della tua azienda sono state aggiornate. + enterprise_fees_destroy_error: "Questa tariffa dell'azienda non può essere annullata perchè è collegata ad una distribuzione: %{id} - %{name}." + enterprise_register_package_error: "Per favore seleziona un pacchetto" + enterprise_register_error: "Non abbiamo potuto completare la registrazione per %{enterprise}" + enterprise_register_success_notice: "Congratulazioni! L registrazione di %{enterprise} è completa!" + enterprise_bulk_update_success_notice: "Aziende aggiornate con successo" + enterprise_bulk_update_error: 'Aggiornamento fallito' + order_cycles_create_notice: 'Il tuo ciclo di richieste è stato creato.' + order_cycles_update_notice: 'Il tuo ciclo di richieste è stato aggiornato' + order_cycles_bulk_update_notice: 'I cicli di richieste sono stati aggiornati.' + order_cycles_clone_notice: "Il tuo ciclo di richieste %{name} è stato duplicato." + order_cycles_email_to_producers_notice: 'Le email da inviare ai produttori sono state messe in coda per l''invio.' + order_cycles_no_permission_to_coordinate_error: "Nessuna delle tue aziende ha il permesso di coordinare un ciclo di richieste" + order_cycles_no_permission_to_create_error: "Non hai il permesso di creare un ciclo di richieste coordinato da questa azienda" + back_to_orders_list: "Indietro alla lista delle gentili richieste" + no_orders_found: "Nessuna gentile richiesta trovata" + order_information: "Informazioni Gentile Richiesta" + amount: "Quantità" + state_names: + ready: Pronto + pending: In sospeso + shipped: Spedito js: + saving: 'Salvataggio...' + changes_saved: 'Modifiche salvate.' + save_changes_first: Salva prima le modifiche. + all_changes_saved: Tutte le modifiche sono state salvate + unsaved_changes: Hai modifiche non salvate + all_changes_saved_successfully: Tutte le modifiche sono state salvate con successo + oh_no: "Oh no! non siamo riusciti a salvare le tue modifiche" + unauthorized: "Non sei autorizzata/o ad accedere a questa pagina." error: Eorrore + unavailable: Non disponibile + profile: Profilo + hub: Hub + shop: Negozio + choose: Scegli + resolve_errors: 'Per favore risolvi i seguenti errori:' + more_items: "+ %{count} ancora" admin: + enterprise_limit_reached: "Hai raggiunto il limite standard di aziende per account. Scrivi a %{contact_email} se hai bisogno di aumentarlo." + modals: + got_it: Capito + close: "Chiuso" + invite: "Invita" + invite_title: "Invita un utente non registrato" + tag_rule_help: + title: Regole Tag + overview: Panoramica + overview_text: > + Le regole per le tag forniscono un modo per definire quali elementi + sono visibili, o a quali utenti. Gli elementi possono essere: metodi + di consegna, metodi di pagamento, prodotti e cicli di richieste. + by_default_rules: "Regole predefinite" + by_default_rules_text: > + Le regole predefinite ti permettono di nascondere gli elementi affinché + non siano visibili. Puoi renderli visibili assegnando regole non predefinite + ad utenti con tag particolari + customer_tagged_rules: "Regole \"Utenti taggati...\"" + customer_tagged_rules_text: > + Creando regole relative ad una specifica tag per clienti, puoi superare + il comportamento predefinito (che prevede elementi visibili o nascosti) + per clienti con le tag specificate. panels: + save: SALVA + saved: SALVATO + saving: SALVATAGGIO + enterprise_package: + hub_profile: Profilo Hub + hub_shop: Isola logistica + profile_only: Solo Profilo + producer_shop: Negozio produttore + producer_hub: Hub produttore + get_listing: Ottieni un listino + always_free: SEMPRE LIBERO + sell_produce_others: Vendi prodotti di altri + sell_own_produce: Vendi i tuoi prodotti + sell_both: Vendi prodotti tuoi e di altri + enterprise_producer: + producer: Produttore + producer_desc: Produttori di cibo + non_producer_desc: Tutti gli altri tipi di Aziende alimentari + non_producer_example: es. Botteghe, Food Coop, GAS enterprise_status: + status_title: "%{name} è impostato e pronto a partire!" description: Descrizione + resolve: Risolvi + new_tag_rule_dialog: + select_rule_type: "Seleziona un tipo di regola:" resend_user_email_confirmation: resend: "Invia nuovamente" + sending: "Invia di nuovo..." + done: "Reinvio fatto ✓" + failed: "Re-invio fallito ✗" + out_of_stock: + reduced_stock_available: Quantità disponibile ridotta + out_of_stock_text: > + Mentre stavi acquistando, le quantità disponibili per uno o più prodotti + nel tuo carrello sono diminuite. Ecco cosa è cambiato: + now_out_of_stock: non è al momento disponibile + only_n_remainging: "attualmente solo %{num} rimasto." + variant_overrides: + inventory_products: "Inventario Prodotti" + hidden_products: "Prodotti Nascosti" + new_products: "Nuovi Prodotti" + reset_stock_levels: Resetta le quantità disponibili alla quantità predefinita + changes_to: Cambia in + no_authorisation: "Non abbiamo l'autorizzazione per salvare queste modifiche. " + some_trouble: "Abbiamo avuto problemi durante il salvataggio: %{errors}" + stock_reset: Quantità resettate alle predefinite. + tag_rules: + show_hide_variants: 'Mostra o nascondi varianti nella mia vetrina' + show_hide_shipping: 'Mostra o nascondi metodi di consegna al checkout' + show_hide_payment: 'Mostra o nascondi metodi di pagamento al checkout' + show_hide_order_cycles: 'Mostra o nascondi cicli di richieste nella mia vetrina' + visible: VISIBILE + not_visible: NON VISIBILE + services: + unsaved_changes_message: 'Ci sono modifiche non salvate: salva ora o ignora?' + save: SALVA + ignore: IGNORA + add_to_order_cycle: "aggiungi al ciclo di richiesta" + manage_products: "gestisci prodotti" + edit_profile: "modifica profilo" + add_products_to_inventory: "aggiungi prodotti all'inventario" + resources: + could_not_delete_customer: 'Non è possibile annullare utente' + order_cycles: + update_success: 'Il tuo ciclo di richieste è stato aggiornato' + enterprises: + producer: "Produttore" + non_producer: "Non-produttore" + customers: + select_shop: 'Seleziona prima un negozio' + could_not_create: Ci dispiace! Non è possibile creare + subscriptions: + closes: chiude + closed: chiuso + close_date_not_set: Data di chiusura non impostata + producers: + signup: + start_free_profile: "Inizia con un profilo gratuito e migliora quando sei pronto!" spree: + email: Email + account_updated: "Account aggiornato!" + my_account: "Il mio account" + date: "Data" + time: "Ora" admin: + orders: + invoice: + code: Codice + from: Da + to: A + form: + distribution_fields: + title: Distribuzione + distributor: "Distributore:" + order_cycle: "Ciclo di richieste:" + overview: + order_cycles: + order_cycles: "Cicli di richiesteCicli di richieste" + order_cycles_tip: "I cicli di richieste determinano dove e quando i tuoi prodotti sono disponibili per i consumatori." + you_have_active: + zero: "Non hai nessun ciclo di richieste attivo." + one: "Hai un ciclo di richieste attivo." + other: "Hai %{count} cicli di richieste attivi." + manage_order_cycles: "GESTISCI CICLI DI RICHIESTE" + payment_methods: + stripe_connect: + enterprise_select_placeholder: Scegli... + status: Stato products: index: products_head: @@ -988,21 +2054,61 @@ it: inherits_properties?: Eredita proprietà? available_on: Disponibile il av_on: "Disp. il" + products_variant: + new_variant: "Nuova variante" + product_name: Nome Prodotto + primary_taxon_form: + product_category: Categoria prodotto + display_as: + display_as: Visualizza come + reports: + table: + select_and_search: "Seleziona i filtri e clicca su CERCA per accedere ai tuoi dati" + users: + email_confirmation: + confirmation_pending: "Email di conferma in sospeso. Abbiamo inviato una mail di conferma a %{address}." + variants: + autocomplete: + producer_name: Produttore general_settings: edit: + legal_settings: "Impostazioni Legali" + enterprises_require_tos: "Le aziende devono accettare i Termini di Servizio" footer_tos_url: "URL Termini di Servizio" + checkout: + payment: + stripe: + choose_one: Scegli uno + date_picker: + js_format: 'aa-mm-gg' + inventory: Inventario + orders: + bought: + item: "Già richiesto in questo ciclo di richieste" + order_mailer: + invoice_email: + hi: "Ciao %{name}" order_state: address: indirizzo adjustments: aggiustamenti awaiting_return: restituzione attesa + canceled: annullato cart: carrello complete: completo confirm: conferma delivery: consegna + paused: in pausa payment: pagamento + pending: in sospeso resumed: ripreso returned: restituito skrill: skrill + subscription_state: + active: attivo + pending: in sospeso + ended: finito + paused: in pausa + canceled: annullato payment_states: balance_due: saldo completed: completato @@ -1020,7 +2126,55 @@ it: pending: in pendenza ready: pronto shipped: spedito + user_mailer: + reset_password_instructions: + request_sent_text: | + E' stata fatta una richiesta per resettare la tua password + Se non hai fatto questa richiesta, ignora semplicemente questa email. + link_text: > + Se hai fatto questa richiesta, clicca il link seguente: + issue_text: | + Se l'URL qui sopra non funziona, prova a copiarlo e incollarlo nel tuo browser. + Se continui ad avere problemi, contattaci. + confirmation_instructions: + subject: Per favore conferma il tuo account OFN + weight: Peso (kg) + zipcode: CAP users: + form: + account_settings: Impostazioni account + show: + tabs: + orders: Gentili richieste + settings: Impostazioni account + unconfirmed_email: "Email di conferma in sospeso per: %{unconfirmed_email}. Il tuo indirizzo email sarà aggiornato quando la nuova email sarà confermata. " + orders: + open_orders: Gentili richieste aperte + past_orders: Gentili richieste passate + transactions: + transaction_history: Storico Transazioni open_orders: + order: Gentile Richiesta + shop: Negozio + changes_allowed_until: Modifiche permesse fino al + items: Prodotti + total: Totale edit: Modifica cancel: Annulla + closed: Chiuso + until: Fino + past_orders: + order: Gentile Richiesta + shop: Negozio + completed_at: Completo al + items: Prodotti + total: Totale + paid?: Pagato? + view: Vista + saved_cards: + default?: Predefinito? + delete?: Elimina? + cards: + authorised_shops: Negozi autorizzati + localized_number: + invalid_format: 'Formato non valido: inserire un numero.' diff --git a/config/locales/pt.yml b/config/locales/pt.yml index cd8c683880..f387287608 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -132,6 +132,7 @@ pt: free_trial: "experimentar sem pagar" plus_tax: "mais IVA" min_bill_turnover_desc: "assim que o volume de negócios exceder %{mbt_amount}" + more: "Mais" say_no: "Não" say_yes: "Sim" then: então @@ -369,6 +370,7 @@ pt: group_signup_page: Página de registo de Grupo footer_and_external_links: Rodapé e Ligações Externas your_content: O seu conteúdo + user_guide: Manual do Utilizador enterprise_fees: index: title: Taxas de Organização @@ -738,6 +740,9 @@ pt: search_placeholder: Procurar por nome manage: Gerir manage_link: Definições + producer?: "Produtor/a?" + package: "Embalagem" + status: "Estado" new_form: owner: Proprietário owner_tip: O utilizador principal responsável por esta organização. @@ -780,6 +785,8 @@ pt: save_reload: Guardar e recarregar página coordinator_fees: add: Adicionar taxa de coordenador + filters: + involving: "Envolvendo" form: incoming: Entrada supplier: Fornecedor @@ -793,7 +800,6 @@ pt: delivery_details: Detalhes de entrega/levantamento debug_info: Informação de depuração index: - involving: Envolvendo schedule: Horário schedules: Horários adding_a_new_schedule: Adicionar um novo Horário @@ -1753,30 +1759,30 @@ pt: you_have_no_orders_yet: "Ainda não tem encomendas" running_balance: "Saldo corrente" outstanding_balance: "Saldo pendente" - admin_entreprise_relationships_everything: "Tudo" - admin_entreprise_relationships_permits: "permite" - admin_entreprise_relationships_seach_placeholder: "Procurar" - admin_entreprise_relationships_button_create: "Criar" - admin_entreprise_groups: "Grupos da Organização" - admin_entreprise_groups_name: "Nome" - admin_entreprise_groups_owner: "Proprietário" - admin_entreprise_groups_on_front_page: "Na página inicial?" - admin_entreprise_groups_entreprise: "Organizações" - admin_entreprise_groups_data_powertip: "O utilizador responsável por este grupo." - admin_entreprise_groups_data_powertip_logo: "Esse é o logo do grupo" - admin_entreprise_groups_data_powertip_promo_image: "Esta imagem aparecerá no topo do perfil do Grupo" - admin_entreprise_groups_contact: "Contacto" - admin_entreprise_groups_contact_phone_placeholder: "ex: 987654321" - admin_entreprise_groups_contact_address1_placeholder: "ex: Rua Alta, 123" - admin_entreprise_groups_contact_city: "Localidade" - admin_entreprise_groups_contact_city_placeholder: "ex. Famalicão" - admin_entreprise_groups_contact_zipcode: "Código Postal " - admin_entreprise_groups_contact_zipcode_placeholder: "ex: 4000-125" - admin_entreprise_groups_contact_state_id: "Região" - admin_entreprise_groups_contact_country_id: "País" - admin_entreprise_groups_web: "Recursos Web" - admin_entreprise_groups_web_twitter: "ex. @nome_perfil" - admin_entreprise_groups_web_website_placeholder: "ex. www.cogumelos.pt" + admin_enterprise_relationships_everything: "Tudo" + admin_enterprise_relationships_permits: "permite" + admin_enterprise_relationships_seach_placeholder: "Procurar" + admin_enterprise_relationships_button_create: "Criar" + admin_enterprise_groups: "Grupos da Organização" + admin_enterprise_groups_name: "Nome" + admin_enterprise_groups_owner: "Proprietário" + admin_enterprise_groups_on_front_page: "Na página inicial?" + admin_enterprise_groups_enterprise: "Organizações" + admin_enterprise_groups_data_powertip: "O utilizador responsável por este grupo." + admin_enterprise_groups_data_powertip_logo: "Esse é o logo do grupo" + admin_enterprise_groups_data_powertip_promo_image: "Esta imagem aparecerá no topo do perfil do Grupo" + admin_enterprise_groups_contact: "Contacto" + admin_enterprise_groups_contact_phone_placeholder: "ex: 987654321" + admin_enterprise_groups_contact_address1_placeholder: "ex: Rua Alta, 123" + admin_enterprise_groups_contact_city: "Localidade" + admin_enterprise_groups_contact_city_placeholder: "ex. Famalicão" + admin_enterprise_groups_contact_zipcode: "Código postal" + admin_enterprise_groups_contact_zipcode_placeholder: "ex: 4000-125" + admin_enterprise_groups_contact_state_id: "Região" + admin_enterprise_groups_contact_country_id: "País" + admin_enterprise_groups_web: "Recursos Web" + admin_enterprise_groups_web_twitter: "ex: @o_prof" + admin_enterprise_groups_web_website_placeholder: "ex: www.cogumelos.pt" admin_order_cycles: "Ciclos de Encomendas do Administrador" open: "Aberto" close: "Fechado" @@ -1911,7 +1917,7 @@ pt: report_payment_totals: 'Totais dos Pagamanetos' report_all: 'todos' report_order_cycle: "Ciclo de Encomendas:" - report_entreprises: "Organizações:" + report_enterprises: "Organizações:" report_users: "Utilizadores:" report_tax_rates: Taxas de imposto report_tax_types: Tipos de imposto diff --git a/config/locales/sv.yml b/config/locales/sv.yml index f5072de5f2..742aabdc17 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -1,4 +1,5 @@ sv: + language_name: "English" activerecord: attributes: spree/order: @@ -236,6 +237,7 @@ sv: group_signup_page: Registreringssida för grupper footer_and_external_links: Sidfot och externa länkar your_content: Ditt innehåll + user_guide: Användarinstruktion enterprise_fees: index: title: Företagsavgifter @@ -507,6 +509,9 @@ sv: search_placeholder: Sök på namn manage: Administrera manage_link: Inställningar + producer?: "Producent?" + package: "Paket" + status: "Status" new_form: owner: Ägare owner_tip: Den primära användaren är ansvarig för detta företag. @@ -1315,30 +1320,30 @@ sv: you_have_no_orders_yet: "Du har inga order ännu" running_balance: "Saldo" outstanding_balance: "Utestående behållning" - admin_entreprise_relationships_everything: "Allting" - admin_entreprise_relationships_permits: "tillåtelse" - admin_entreprise_relationships_seach_placeholder: "Sök" - admin_entreprise_relationships_button_create: "Skapa" - admin_entreprise_groups: "Företagsgrpper" - admin_entreprise_groups_name: "Namn" - admin_entreprise_groups_owner: "Ägare" - admin_entreprise_groups_on_front_page: "På första sidan?" - admin_entreprise_groups_entreprise: "Företag" - admin_entreprise_groups_data_powertip: "Huvudansvarig för denna grupp." - admin_entreprise_groups_data_powertip_logo: "Detta är logotypen för gruppen" - admin_entreprise_groups_data_powertip_promo_image: "Denna bild visas överst på gruppens profil" - admin_entreprise_groups_contact: "Kontakt" - admin_entreprise_groups_contact_phone_placeholder: "t.ex. 98 7654 3210" - admin_entreprise_groups_contact_address1_placeholder: "t.ex. 123 Höga gatan" - admin_entreprise_groups_contact_city: "Förort" - admin_entreprise_groups_contact_city_placeholder: "ex Norrberga" - admin_entreprise_groups_contact_zipcode: "Postnummer" - admin_entreprise_groups_contact_zipcode_placeholder: "t.ex. 3070" - admin_entreprise_groups_contact_state_id: "Region" - admin_entreprise_groups_contact_country_id: "Land" - admin_entreprise_groups_web: "Webb resurser" - admin_entreprise_groups_web_twitter: "ex @the_prof" - admin_entreprise_groups_web_website_placeholder: "ex www.truffles.com" + admin_enterprise_relationships_everything: "Allting" + admin_enterprise_relationships_permits: "tillåtelse" + admin_enterprise_relationships_seach_placeholder: "Sök" + admin_enterprise_relationships_button_create: "Skapa" + admin_enterprise_groups: "Företagsgrpper" + admin_enterprise_groups_name: "Namn" + admin_enterprise_groups_owner: "Ägare" + admin_enterprise_groups_on_front_page: "På första sidan?" + admin_enterprise_groups_enterprise: "Företag" + admin_enterprise_groups_data_powertip: "Huvudansvarig för denna grupp." + admin_enterprise_groups_data_powertip_logo: "Detta är logotypen för gruppen" + admin_enterprise_groups_data_powertip_promo_image: "Denna bild visas överst på gruppens profil" + admin_enterprise_groups_contact: "Kontakt" + admin_enterprise_groups_contact_phone_placeholder: "ex 123 456 789" + admin_enterprise_groups_contact_address1_placeholder: "eg. 123 High Street" + admin_enterprise_groups_contact_city: "Stadsdel" + admin_enterprise_groups_contact_city_placeholder: "eg. Northcote" + admin_enterprise_groups_contact_zipcode: "Postkod" + admin_enterprise_groups_contact_zipcode_placeholder: "eg. 3070" + admin_enterprise_groups_contact_state_id: "Region" + admin_enterprise_groups_contact_country_id: "Land" + admin_enterprise_groups_web: "Webb resurser" + admin_enterprise_groups_web_twitter: "t.ex. @the_prof" + admin_enterprise_groups_web_website_placeholder: "ex www.shop.com" admin_order_cycles: "Administratörens order cykel" open: "Öppen" close: "Stängd" @@ -1462,7 +1467,7 @@ sv: report_payment_totals: 'Betalningstotaler' report_all: 'alla' report_order_cycle: "Beställningsrundor" - report_entreprises: "Företag:" + report_enterprises: "Företag:" report_users: "Användare:" report_tax_rates: Skattesatser report_tax_types: Typer av skatt From 897e43fe0b082080319daf66fc141040fe266391 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Thu, 20 Sep 2018 11:33:22 +0100 Subject: [PATCH 113/190] Remove Spree's Deface data-hooks from new view --- app/views/spree/admin/orders/index.html.haml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index ed94800e02..526c4c5ee4 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -16,11 +16,11 @@ - content_for :table_filter do = render partial: 'filters' -%table#listing_orders.index.responsive{"data-hook" => "", width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } +%table#listing_orders.index.responsive{width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } %colgroup %col{style: "width: 10%"} %thead - %tr{"data-hook" => "admin_orders_index_headers"} + %tr %th = t(:products_distributor) - if @show_only_completed @@ -65,9 +65,9 @@ = t(:total, scope: 'activerecord.attributes.spree/order') %span{'ng-show' => "sorting == 'total asc'"}= "▲".html_safe %span{'ng-show' => "sorting == 'total desc'"}= "▼".html_safe - %th.actions{"data-hook" => "admin_orders_index_header_actions"} + %th.actions %tbody - %tr{ng: {repeat: 'order in orders track by $index', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'", "data-hook" => "admin_orders_index_rows"} + %tr{ng: {repeat: 'order in orders track by $index', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'"} %td.align-center {{::order.distributor_name}} %td.align-center @@ -94,7 +94,7 @@ = mail_to "{{::order.email}}" %td.align-center %span{'ng-bind-html' => '::order.display_total'} - %td.actions{"data-hook" => "admin_orders_index_row_actions"} + %td.actions %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{::order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t(:edit)} %div{'ng-if' => 'order.ready_to_ship'} %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{::order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t(:ship)} From 2a68184c01fd476ceef2a43bfbcfecad4f86faae Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Thu, 20 Sep 2018 20:09:14 +0100 Subject: [PATCH 114/190] Deleted unused test helper for rabl views --- spec/support/views/rabl_helper.rb | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 spec/support/views/rabl_helper.rb diff --git a/spec/support/views/rabl_helper.rb b/spec/support/views/rabl_helper.rb deleted file mode 100644 index 295a2ef07d..0000000000 --- a/spec/support/views/rabl_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -module RablHelper - # See https://github.com/nesquena/rabl/issues/231 - # Allows us to test RABL views using URL helpers - class FakeContext - include Singleton - include Rails.application.routes.url_helpers - include Sprockets::Helpers::RailsHelper - include Sprockets::Helpers::IsolatedHelper - include ActionView::Helpers::TagHelper - include ActionView::Helpers::AssetTagHelper - end -end From 33d2b65d65b77ad65e04232f57d105623ff31c19 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Thu, 20 Sep 2018 23:01:23 +0100 Subject: [PATCH 115/190] Removed deprecated spec helper have_select2_option and fixed its last usage --- spec/features/admin/orders_spec.rb | 2 +- spec/support/request/web_helper.rb | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 0b9a6cb602..b9bfd9ad6e 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -128,7 +128,7 @@ feature %q{ visit '/admin/orders' page.find('td.actions a.icon-edit').click - expect(page).not_to have_select2_option product.name, from: ".variant_autocomplete", dropdown_css: ".select2-search" + expect(page).not_to have_select2 "add_variant_id", with_options: [product.name] end scenario "can't change distributor or order cycle once order has been finalized" do diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index 5e1712b131..71e43599f4 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -168,15 +168,6 @@ module WebHelper select_select2_result(value) end - # Deprecated: Use have_select2 instead (spec/support/matchers/select2_matchers.rb) - def have_select2_option(value, options) - container = options[:dropdown_css] || ".select2-with-searchbox" - page.execute_script %Q{$('#{options[:from]}').select2('open')} - page.execute_script "$('#{container} input.select2-input').val('#{value}').trigger('keyup-change');" - sleep 1 - have_selector "div.select2-result-label", text: value - end - def open_select2(selector) page.execute_script "jQuery('#{selector}').select2('open');" end From 7175434910e023361b97a7858efbb8881449a1db Mon Sep 17 00:00:00 2001 From: Transifex-Openfoodnetwork Date: Sat, 22 Sep 2018 01:25:48 +1000 Subject: [PATCH 116/190] Updating translations for config/locales/en_US.yml --- config/locales/en_US.yml | 243 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 242 insertions(+), 1 deletion(-) diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 167ed6aaaa..d1f41a8849 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -18,6 +18,8 @@ en_US: attributes: email: taken: "There's already an account registered for this email. Please login to reset your password." + spree/order: + no_card: There are no authorized credit cards available to charge order_cycle: attributes: orders_close_at: @@ -39,12 +41,14 @@ en_US: payment_method: not_available_to_shop: "is not available to %{shop}" invalid_type: "must be a Cash or Stripe method" + charges_not_allowed: "^Credit card charges are not allowed by this customer" + no_default_card: "^No default card available for this customer" shipping_method: not_available_to_shop: "is not available to %{shop}" devise: confirmations: send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." - failed_to_send: "An error occurred whilst sending your confirmation email." + failed_to_send: "An error occurred while sending your confirmation email." resend_confirmation_email: "Resend confirmation email." confirmed: "Thanks for confirming your email! You can now log in." not_confirmed: "Your email address could not be confirmed. Perhaps you have already completed this step?" @@ -56,6 +60,13 @@ en_US: Invalid email or password. Were you a guest last time? Perhaps you need to create an account or reset your password. unconfirmed: "You have to confirm your account before continuing." + already_registered: "This email address is already registered. Please log in to continue, or go back and use another email address." + user_passwords: + spree_user: + updated_not_active: "Your password has been reset, but your email has not been confirmed yet." + models: + order_cycle: + cloned_order_cycle_name: "COPY OF %{order_cycle}" enterprise_mailer: confirmation_instructions: subject: "Please confirm the email address for %{enterprise}" @@ -63,9 +74,26 @@ en_US: subject: "%{enterprise}is now on %{sitename}" invite_manager: subject: "%{enterprise} has invited you to be a manager" + order_mailer: + cancel_email: + dear_customer: "Dear Customer," + instructions: "Your order has been CANCELED. Please retain this cancellation information for your records." + order_summary_canceled: "Order Summary [CANCELED]" + subject: "Cancellation of Order" + subtotal: "Subtotal: %{subtotal}" + total: "Order Total: %{total}" producer_mailer: order_cycle: subject: "Order cycle report for %{producer}" + shipment_mailer: + shipped_email: + dear_customer: "Dear Customer," + instructions: "Your order has been shipped" + shipment_summary: "Shipment Summary" + subject: "Shipment Notification" + thanks: "Thank you for your business." + track_information: "Tracking Information: %{tracking}" + track_link: "Tracking Link: %{url}" subscription_mailer: placement_summary_email: subject: A summary of recently placed subscription orders @@ -128,6 +156,7 @@ en_US: free_trial: "free trial" plus_tax: "plus tax" min_bill_turnover_desc: "once turnover exceeds %{mbt_amount}" + more: "More" say_no: "No" say_yes: "Yes" then: then @@ -229,6 +258,7 @@ en_US: form_invalid: "Form contains missing or invalid fields" clear_filters: Clear Filters clear: Clear + save: Save cancel: Cancel back: Back show_more: Show more @@ -330,6 +360,13 @@ en_US: charges_enabled_warning: "Warning: Charges are not enabled for your account" auth_fail_error: The API key you provided is invalid empty_api_key_error_html: No Stripe API key has been provided. To set your API key, please follow these instructions + matomo_settings: + edit: + title: "Matomo Settings" + matomo_url: "Matomo URL" + matomo_site_id: "Matomo Site ID" + info_html: "Matomo is a Web and Mobile Analytics. You can either host Matomo locally or use a cloud-hosted service. See matomo.org for more information." + config_instructions_html: "Here you can configure the OFN Matomo integration. The Matomo URL below should point to the Matomo instance where the user tracking information will be sent to; if it is left empty, Matomo user tracking will be disabled. The Site ID field is not mandatory but useful if you are tracking more than one website on a single Matomo instance; it can be found on the Matomo instance console." customers: index: add_customer: "Add Customer" @@ -352,6 +389,7 @@ en_US: update_address: 'Update Address' confirm_delete: 'Sure to delete?' search_by_email: "Search by email/code..." + guest_label: 'Guest checkout' destroy: has_associated_orders: 'Delete failed: customer has associated orders with his shop' contents: @@ -362,6 +400,7 @@ en_US: producer_signup_page: Producer signup page hub_signup_page: Hub signup page group_signup_page: Group signup page + main_links: Main Menu Links footer_and_external_links: Footer and External Links your_content: Your content user_guide: User Guide @@ -392,6 +431,7 @@ en_US: inherits_properties?: Inherits Properties? available_on: Available On av_on: "Av. On" + import_date: Imported upload_an_image: Upload an image product_search_keywords: Product Search Keywords product_search_tip: Type words to help search your products in the shops. Use space to separate each keyword. @@ -407,13 +447,87 @@ en_US: group_buy_options: "Group Buy Options" back_to_products_list: "Back to products list" product_import: + title: Product Import file_not_found: File not found or could not be opened no_data: No data found in spreadsheet + confirm_reset: "This will set stock level to zero on all products for this \n enterprise that are not present in the uploaded file" model: no_file: "error: no file uploaded" could_not_process: "could not process file: invalid filetype" + incorrect_value: incorrect value + conditional_blank: can't be blank if unit_type is blank + no_product: did not match any products in the database + not_found: not found in database blank: can't be blank + products_no_permission: you do not have permission to manage products for this enterprise + inventory_no_permission: you do not have permission to create inventory for this producer none_saved: did not save any products successfully + line: Line + index: + select_file: Select a spreadsheet to upload + spreadsheet: Spreadsheet + choose_import_type: Select import type + import_into: Import type + product_list: Product list + inventories: Inventories + import: Import + upload: Upload + csv_templates: CSV Templates + product_list_template: Download Product List template + inventory_template: Download Inventory template + category_values: Available Category Values + product_categories: Product Categories + tax_categories: Tax Categories + shipping_categories: Shipping Categories + import: + review: Review + import: Import + save: Save + results: Results + save_imported: Save imported products + no_valid_entries: No valid entries found + none_to_save: There are no entries that can be saved + some_invalid_entries: Imported file contains invalid entries + fix_before_import: Please fix these errors and try importing the file again + save_valid?: Save valid entries for now and discard the others? + no_errors: No errors detected! + save_all_imported?: Save all imported products? + options_and_defaults: Import options and defaults + no_permission: you do not have permission to manage this enterprise + not_found: enterprise could not be found in database + no_name: No name + blank_supplier: some products have blank supplier name + reset_absent?: Reset absent products + overwrite_all: Overwrite all + overwrite_empty: Overwrite if empty + default_stock: Set stock level + default_tax_cat: Set tax category + default_shipping_cat: Set shipping category + default_available_date: Set available date + validation_overview: Import validation overview + entries_with_errors: Items contain errors and will not be imported + products_to_create: Products will be created + products_to_update: Products will be updated + inventory_to_create: Inventory items will be created + inventory_to_update: Inventory items will be updated + products_to_reset: Existing products will have their stock reset to zero + inventory_to_reset: Existing inventory items will have their stock reset to zero + line: Line + item_line: Item line + save_results: + final_results: Import final results + products_created: Products created + products_updated: Products updated + inventory_created: Inventory items created + inventory_updated: Inventory items updated + products_reset: Products had stock level reset to zero + inventory_reset: Inventory items had stock level reset to zero + all_saved: "All items saved successfully" + some_saved: "items saved successfully" + save_errors: Save errors + import_again: Upload Another File + view_products: Go To Products Page + view_inventory: Go To Inventory Page variant_overrides: loading_flash: loading_inventory: LOADING INVENTORY @@ -424,6 +538,7 @@ en_US: inherit?: Inherit? add: Add hide: Hide + import_date: Imported 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. @@ -680,14 +795,24 @@ en_US: i_am_producer: I am a Producer contact_name: Contact Name edit: + editing: 'Settings:' back_link: Back to enterprises list new: title: New Enterprise back_link: Back to enterprises list + remove_logo: + remove: "Remove Image" + removed_successfully: "Logo removed successfully" + immediate_removal_warning: "The logo will be removed immediately after you confirm." + remove_promo_image: + remove: "Remove Image" + removed_successfully: "Promo image removed successfully" + immediate_removal_warning: "The promo image will be removed immediately after you confirm." welcome: welcome_title: Welcome to the Open Food Network! welcome_text: You have successfully created a next_step: Next step + choose_starting_point: 'Choose your package:' invite_manager: user_already_exists: "User already exists" error: "Something went wrong" @@ -716,7 +841,10 @@ en_US: coordinator_fees: add: Add coordinator fee filters: + search_by_order_cycle_name: "Search by Order Cycle name..." involving: "Involving" + any_enterprise: "Any Enterprise" + any_schedule: "Any Schedule" form: incoming: Incoming supplier: Supplier @@ -766,7 +894,9 @@ en_US: bulk_update: no_data: Hm, something went wrong. No order cycle data found. date_warning: + msg: This order cycle is linked to %{n} open subscription orders. Changing this date now will not affect any orders which have already been placed, but should be avoided if possible. Are you sure you want to proceed? cancel: Cancel + proceed: Proceed producer_properties: index: title: Producer Properties @@ -871,11 +1001,18 @@ en_US: address: 2. Address products: 3. Add Products review: 4. Review & Save + subscription_line_items: + this_is_an_estimate: | + The displayed prices are only an estimate and calculated at the time the subscription is changed. + If you change prices or fees, orders will be updated, but the subscription will still display the old values. details: details: Details invalid_error: Oops! Please fill in all of the required fields... allowed_payment_method_types_tip: Only Cash and Stripe payment methods may be used at the moment credit_card: Credit Card + charges_not_allowed: Charges are not allowed by this customer + no_default_card: Customer has no cards available to charge + card_ok: Customer has a card available to charge loading_flash: loading: LOADING SUBSCRIPTIONS review: @@ -912,6 +1049,11 @@ en_US: stripe_connect_fail: Sorry, the connection of your Stripe account failed stripe_connect_settings: resource: Stripe Connect configuration + api: + enterprise_logo: + destroy_attachment_does_not_exist: "Logo does not exist" + enterprise_promo_image: + destroy_attachment_does_not_exist: "Promo image does not exist" checkout: already_ordered: cart: "cart" @@ -951,6 +1093,11 @@ en_US: footer_legal_tos: "Terms and conditions" footer_legal_visit: "Find us on" footer_legal_text_html: "Open Food Network is a free and open source software platform. Our content is licensed with %{content_license} and our code with %{code_license}." + footer_data_text_with_privacy_policy_html: "We take good care of your data. See our %{privacy_policy} and %{cookies_policy}" + footer_data_text_without_privacy_policy_html: "We take good care of your data. See our %{cookies_policy}" + footer_data_privacy_policy: "privacy policy" + footer_data_cookies_policy: "cookies policy" + footer_skylight_dashboard_html: Performance data is available on %{dashboard}. shop: messages: login: "login" @@ -959,6 +1106,7 @@ en_US: 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." + card_could_not_be_updated: Card could not be updated card_could_not_be_saved: card could not be saved spree_gateway_error_flash_for_checkout: "There was a problem with your payment information: %{error}" invoice_billing_address: "Billing address:" @@ -986,12 +1134,19 @@ en_US: ticket_column_unit_price: "Unit Price" ticket_column_total_price: "Total Price" menu_1_title: "Stores" + menu_1_url: "/shops" menu_2_title: "Map" + menu_2_url: "/map" menu_3_title: "Producers" + menu_3_url: "/producers" menu_4_title: "Groups" + menu_4_url: "/groups" menu_5_title: "About" + menu_5_url: "http://www.openfoodnetwork.org/" menu_6_title: "Connect" + menu_6_url: "https://openfoodnetwork.org/au/connect/" menu_7_title: "Learn" + menu_7_url: "https://openfoodnetwork.org/au/learn/" logo: "Logo (640x130)" logo_mobile: "Mobile logo (75x26)" logo_mobile_svg: "Mobile logo (SVG)" @@ -1007,6 +1162,7 @@ en_US: footer_email: "Email" footer_links_md: "Links" footer_about_url: "About URL" + user_guide_link: "User Guide Link" name: Name first_name: First name last_name: Last name @@ -1080,6 +1236,48 @@ en_US: ie_warning_firefox: Download Firefox ie_warning_ie: Upgrade Internet Explorer ie_warning_other: "Can't upgrade your browser? Try Open Food Network on your smartphone :-)" + legal: + cookies_policy: + header: "How We Use Cookies" + desc_part_1: "Cookies are very small text files that are stored on your computer when you visit some websites." + desc_part_2: "In OFN we are fully respectful of your privacy. We use only the cookies that are necessary for delivering you the service of selling/buying food online. We don’t sell any of your data. We might in the future propose you to share some of your data to build new commons services that could be useful for the ecosystem (like logistics services for short food systems) but we are not yet there, and we won’t do it without your authorization :-)" + desc_part_3: "We use cookies mainly to remember who you are if you 'log in' to the service, or to be able to remember the items you put in your cart even if you are not logged in. If you keep navigating on the website without clicking on “Accept cookies”, we assume you are giving us consent to store the cookies that are essential for the functioning of the website. Here is the list of cookies we use!" + essential_cookies: "Essential Cookies" + essential_cookies_desc: "The following cookies are strictly necessary for the operation of our website." + essential_cookies_note: "Most cookies only contain a unique identifier, but no other data, so your email address and password for instance are never contained in them and never exposed." + cookie_domain: "Set By:" + cookie_session_desc: "Used to allow the website to remember users between page visits, for example, remember items in your cart." + cookie_consent_desc: "Used to maintain status of user consent to store cookies" + cookie_remember_me_desc: "Used if the user has requested the website to remember him. This cookie is automatically deleted after 12 days. If as a user you want that cookie to be deleted, you only need to logout. If you don’t want that cookie to be installed on your computer you shouldn’t check the “remember me” checkbox when logging in." + cookie_openstreemap_desc: "Used by our friendly open source mapping provider (OpenStreetMap) to ensure that it does not receive too many requests during a given time period, to prevent abuse of their services." + cookie_stripe_desc: "Data collected by our payment processor Stripe for fraud detection https://stripe.com/cookies-policy/legal. Not all shops use Stripe as a payment method but it is a good practice to prevent fraud to apply it to all pages. Stripe probably build a picture of which of our pages usually interact with their API and then flag anything unusual. So setting the Stripe cookie has a broader function than simply the provision of a payment method to a user. Removing it could affect the security of the service itself. You can learn more about Stripe and read its privacy policy at https://stripe.com/privacy." + statistics_cookies: "Statistics Cookies" + statistics_cookies_desc: "The following are not strictly necessary, but help to provide you with the best user experience by allowing us to analyse user behavior, identify which features you use most, or don’t use, understand user experience issues, etc." + statistics_cookies_analytics_desc_html: "To collect and analyze platform usage data, we use Google Analytics, as it was the default service connected with Spree (the e-commerce open source software that we built on) but our vision is to switch to Matomo (ex Piwik, open source analytics tool that is GDPR compliant and protects your privacy) as soon as we can." + statistics_cookies_matomo_desc_html: "To collect and analyze platform usage data, we use Matomo (ex Piwik), an open source analytics tool that is GDPR compliant and protects your privacy." + statistics_cookies_matomo_optout: "Do you want to opt-out of Matomo analytics? We don’t collect any personal data, and Matomo helps us to improve our service, but we respect your choice :-)" + cookie_analytics_utma_desc: "Used to distinguish users and sessions. The cookie is created when the javascript library executes and no existing __utma cookies exists. The cookie is updated every time data is sent to Google Analytics." + cookie_analytics_utmt_desc: "Used to throttle request rate." + cookie_analytics_utmb_desc: "Used to determine new sessions/visits. The cookie is created when the javascript library executes and no existing __utmb cookies exists. The cookie is updated every time data is sent to Google Analytics." + cookie_analytics_utmc_desc: "Not used in ga.js. Set for interoperability with urchin.js. Historically, this cookie operated in conjunction with the __utmb cookie to determine whether the user was in a new session/visit." + cookie_analytics_utmz_desc: "Stores the traffic source or campaign that explains how the user reached your site. The cookie is created when the javascript library executes and is updated every time data is sent to Google Analytics." + cookie_matomo_basics_desc: "Matomo first party cookies to collect statistics." + cookie_matomo_heatmap_desc: "Matomo Heatmap & Session Recording cookie." + cookie_matomo_ignore_desc: "Cookie used to exclude user from being tracked." + disabling_cookies_header: "Warning on disabling cookies" + disabling_cookies_desc: "As a user you can always allow, block or delete Open Food Network’s or any other website cookies whenever you want to through your browser’s setting control. Each browser has a different operative. Here are the links:" + disabling_cookies_firefox_link: "https://support.mozilla.org/en-US/kb/enable-and-disable-cookies-website-preferences" + disabling_cookies_chrome_link: "https://support.google.com/chrome/answer/95647" + disabling_cookies_ie_link: "https://support.microsoft.com/en-us/help/17442/windows-internet-explorer-delete-manage-cookies" + disabling_cookies_safari_link: "https://www.apple.com/legal/privacy/en-ww/cookies/" + disabling_cookies_note: "But be aware that if you delete or modify the essential cookies used by Open Food Network, the website won’t work, you will not be able to add anything to your cart or to check out, for instance." + cookies_banner: + cookies_usage: "This site uses cookies in order to make your navigation frictionless and secure, and to help us understand how you use it in order to improve the features we offer." + cookies_definition: "Cookies are very small text files that are stored on your computer when you visit some websites." + cookies_desc: "We use only the cookies that are necessary for delivering you the service of selling/buying food online. We don’t sell any of your data. We use cookies mainly to remember who you are if you ‘log in’ to the service, or to be able to remember the items you put in your cart even if you are not logged in. If you keep navigating on the website without clicking on “Accept cookies”, we assume you are giving us consent to store the cookies that are essential for the functioning of the website." + cookies_policy_link_desc: "If you want to learn more, check our" + cookies_policy_link: "cookies policy" + cookies_accept_button: "Accept Cookies" home_shop: Shop Now brandstory_headline: "Food, unincorporated." brandstory_intro: "Sometimes the best way to fix the system is to start a new one…" @@ -1169,6 +1367,7 @@ en_US: email_confirmation_click_link: "Please click the link below to confirm your email and to continue setting up your profile." email_confirmation_link_label: "Confirm this email address »" email_confirmation_help_html: "After confirming your email you can access your administration account for this enterprise. See the %{link} to find out more about %{sitename}'s features and to start using your profile or online store." + email_confirmation_notice_unexpected: "You received this message because you signed up on %{sitename}, or were invited to sign up by someone you probably know. If you don't understand why you are receiving this email, please write to %{contact}." email_social: "Connect with Us:" email_contact: "Email us:" email_signoff: "Cheers," @@ -1199,6 +1398,7 @@ en_US: email_so_edit_true_html: "You can make changes until orders close on %{orders_close_at}." email_so_edit_false_html: "You can view details of this order at any time." email_so_contact_distributor_html: "If you have any questions you can contact %{distributor} via %{email}." + email_so_contact_distributor_to_change_order_html: "This order was automatically created for you. You can make changes until orders close on %{orders_close_at} by contacting %{distributor} via %{email}." email_so_confirmation_intro_html: "Your order with %{distributor} is now confirmed" email_so_confirmation_explainer_html: "This order was automatically placed for you, and it has now been finalised." email_so_confirmation_details_html: "Here's everything you need to know about your order from %{distributor}:" @@ -1264,6 +1464,9 @@ en_US: hubs_intro: Shop in your local area hubs_distance: Closest to hubs_distance_filter: "Show me shops near %{location}" + shop_changeable_orders_alert_html: + one: Your order with %{shop} / %{order} is open for review. You can make changes until %{oc_close}. + other: You have %{count} orders with %{shop} currently open for review. You can make changes until %{oc_close}. orders_changeable_orders_alert_html: This order has been confirmed, but you can make changes until %{oc_close}. products_clear_all: Clear all products_showing: "Showing:" @@ -1411,6 +1614,9 @@ en_US: orders_your_order_has_been_cancelled: "Your order has been cancelled" orders_could_not_cancel: "Sorry, the order could not be cancelled" orders_cannot_remove_the_final_item: "Cannot remove the final item from an order, please cancel the order instead." + orders_bought_items_notice: + one: "An additional item is already confirmed for this order cycle" + other: "%{count} additional items already confirmed for this order cycle" orders_bought_edit_button: Edit confirmed items orders_bought_already_confirmed: "* already confirmed" orders_confirm_cancel: Are you sure you want to cancel this order? @@ -1454,6 +1660,7 @@ en_US: error_number: "must be a number" error_email: "must be email address" error_not_found_in_database: "%{name} not found in database" + error_not_primary_producer: "%{name} is not enabled as a producer" error_no_permission_for_enterprise: "\"%{name}\": you do not have permission to manage products for this enterprise" item_handling_fees: "Item Handling Fees (included in item totals)" january: "January" @@ -1469,6 +1676,7 @@ en_US: november: "November" december: "December" email_not_found: "Email address not found" + email_unconfirmed: "You must confirm your email address before you can reset your password." email_required: "You must provide an email address" logging_in: "Hold on a moment, we're logging you in" signup_email: "Your email" @@ -1571,6 +1779,7 @@ en_US: enterprise_about_headline: "Nice one!" enterprise_about_message: "Now let's flesh out the details about" enterprise_success: "Success! %{enterprise} added to the Open Food Network " + enterprise_registration_exit_message: "If you exit this wizard at any stage, you can continue to create your profile by going to the admin interface." enterprise_description: "Short Description" enterprise_description_placeholder: "A short sentence describing your enterprise" enterprise_long_desc: "Long Description" @@ -1678,6 +1887,7 @@ en_US: you_have_no_orders_yet: "You have no orders yet" running_balance: "Running balance" outstanding_balance: "Outstanding balance" + admin_enterprise_relationships: "Enterprise Permissions" admin_enterprise_relationships_everything: "Everything" admin_enterprise_relationships_permits: "permits" admin_enterprise_relationships_seach_placeholder: "Search" @@ -1801,6 +2011,7 @@ en_US: edit_profile_details_etc: "Change your profile description, images, etc." order_cycle: "Order Cycle" order_cycles: "Order Cycles" + enterprise_relationships: "Enterprise permissions" remove_tax: "Remove tax" enterprise_tos_link: "Enterprise Terms of Service link" enterprise_tos_message: "We want to work with people that share our aims and values. As such we ask new enterprises to agree to our " @@ -2021,6 +2232,7 @@ en_US: content_configuration_pricing_table: "(TODO: Pricing table)" content_configuration_case_studies: "(TODO: Case studies)" content_configuration_detail: "(TODO: Detail)" + enterprise_name_error: "has already been taken. If this is your enterprise and you would like to claim ownership, or if you would like to trade with this enterprise please contact the current manager of this profile at %{email}." enterprise_owner_error: "^%{email} is not permitted to own any more enterprises (limit is %{enterprise_limit})." enterprise_role_uniqueness_error: "^That role is already present." inventory_item_visibility_error: must be true or false @@ -2055,6 +2267,7 @@ en_US: order_cycles_no_permission_to_coordinate_error: "None of your enterprises have permission to coordinate an order cycle" order_cycles_no_permission_to_create_error: "You don't have permission to create an order cycle coordinated by that enterprise" back_to_orders_list: "Back to order list" + no_orders_found: "No Orders Found" order_information: "Order Information" date_completed: "Date Completed" amount: "Amount" @@ -2079,6 +2292,7 @@ en_US: choose: Choose resolve_errors: Please resolve the following errors more_items: "+ %{count} More" + default_card_updated: Default Card Updated admin: enterprise_limit_reached: "You have reached the standard limit of enterprises per account. Write to %{contact_email} if you need to increase it." modals: @@ -2200,6 +2414,9 @@ en_US: select_rule_type: "Select a rule type:" resend_user_email_confirmation: resend: "Resend" + sending: "Resend..." + done: "Resend done ✓" + failed: "Resend failed ✗" out_of_stock: reduced_stock_available: Reduced stock available out_of_stock_text: > @@ -2243,7 +2460,9 @@ en_US: This will set stock level to zero on all products for this enterprise that are not present in the uploaded file. order_cycles: + create_failure: "Failed to create order cycle" update_success: 'Your order cycle has been updated.' + update_failure: "Failed to update order cycle" no_distributors: There are no distributors in this order cycle. This order cycle will not be visible to customers until you add one. Would you like to continue saving this order cycle?' enterprises: producer: "Producer" @@ -2264,7 +2483,14 @@ en_US: my_account: "My account" date: "Date" time: "Time" + layouts: + admin: + header: + store: Store admin: + product_properties: + index: + inherits_properties_checkbox_hint: "Inherit properties from %{supplier}? (unless overridden above)" orders: invoice: issued_on: Issued on @@ -2300,6 +2526,11 @@ en_US: account_id: Account ID business_name: Business Name charges_enabled: Charges Enabled + payments: + source_forms: + stripe: + error_saving_payment: Error saving payment + submitting_payment: Submitting payment... products: new: title: 'New Product' @@ -2332,17 +2563,27 @@ en_US: display_as: display_as: Display As reports: + table: + select_and_search: "Select filters and click on SEARCH to access your data." bulk_coop: bulk_coop_supplier_report: 'Bulk Co-op - Totals by Supplier' bulk_coop_allocation: 'Bulk Co-op - Allocation' bulk_coop_packing_sheets: 'Bulk Co-op - Packing Sheets' bulk_coop_customer_payments: 'Bulk Co-op - Customer Payments' + users: + email_confirmation: + confirmation_pending: "Email confirmation is pending. We've sent a confirmation email to %{address}." variants: autocomplete: producer_name: Producer general_settings: edit: + legal_settings: "Legal Settings" + cookies_consent_banner_toggle: "Display cookies consent banner" + privacy_policy_url: "Privacy Policy URL" enterprises_require_tos: "Enterprises must accept Terms of Service" + cookies_policy_matomo_section: "Display Matomo section on cookies policy page" + cookies_policy_ga_section: "Display Google Analytics section on cookies policy page" footer_tos_url: "Terms of Service URL" checkout: payment: From 9f57b43a13db3927ae5af38701583932d763d151 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Sun, 23 Sep 2018 22:14:12 +0100 Subject: [PATCH 117/190] Move sortble header elements to a partial --- .../admin/orders/_sortable_header.html.haml | 4 ++ app/views/spree/admin/orders/index.html.haml | 38 ++----------------- 2 files changed, 8 insertions(+), 34 deletions(-) create mode 100644 app/views/spree/admin/orders/_sortable_header.html.haml diff --git a/app/views/spree/admin/orders/_sortable_header.html.haml b/app/views/spree/admin/orders/_sortable_header.html.haml new file mode 100644 index 0000000000..97d24165c0 --- /dev/null +++ b/app/views/spree/admin/orders/_sortable_header.html.haml @@ -0,0 +1,4 @@ +%a{'ng-click' => "sortOptions.toggle('#{column_name}')"} + = t(column_name.to_s, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == '#{column_name} asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == '#{column_name} desc'"}= "▼".html_safe \ No newline at end of file diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 526c4c5ee4..dec3c2c576 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -31,40 +31,10 @@ %span{'ng-show' => "sorting == 'completed_at desc' || sorting === undefined"}= "▼".html_safe - else %th - %a{'ng-click' => "sortOptions.toggle('created_at')"} - = t(:created_at, scope: 'activerecord.attributes.spree/order') - %span{'ng-show' => "sorting == 'created_at asc'"}= "▲".html_safe - %span{'ng-show' => "sorting == 'created_at desc'"}= "▼".html_safe - %th - %a{'ng-click' => "sortOptions.toggle('number')"} - = t(:number, scope: 'activerecord.attributes.spree/order') - %span{'ng-show' => "sorting == 'number asc'"}= "▲".html_safe - %span{'ng-show' => "sorting == 'number desc'"}= "▼".html_safe - %th - %a{'ng-click' => "sortOptions.toggle('state')"} - = t(:state, scope: 'activerecord.attributes.spree/order') - %span{'ng-show' => "sorting == 'state asc'"}= "▲".html_safe - %span{'ng-show' => "sorting == 'state desc'"}= "▼".html_safe - %th - %a{'ng-click' => "sortOptions.toggle('payment_state')"} - = t(:payment_state, scope: 'activerecord.attributes.spree/order') - %span{'ng-show' => "sorting == 'payment_state asc'"}= "▲".html_safe - %span{'ng-show' => "sorting == 'payment_state desc'"}= "▼".html_safe - %th - %a{'ng-click' => "sortOptions.toggle('shipment_state')"} - = t(:shipment_state, scope: 'activerecord.attributes.spree/order') - %span{'ng-show' => "sorting == 'shipment_state asc'"}= "▲".html_safe - %span{'ng-show' => "sorting == 'shipment_state desc'"}= "▼".html_safe - %th - %a{'ng-click' => "sortOptions.toggle('email')"} - = t(:email, scope: 'activerecord.attributes.spree/order') - %span{'ng-show' => "sorting == 'email asc'"}= "▲".html_safe - %span{'ng-show' => "sorting == 'email desc'"}= "▼".html_safe - %th - %a{'ng-click' => "sortOptions.toggle('total')"} - = t(:total, scope: 'activerecord.attributes.spree/order') - %span{'ng-show' => "sorting == 'total asc'"}= "▲".html_safe - %span{'ng-show' => "sorting == 'total desc'"}= "▼".html_safe + = render partial: 'sortable_header', locals: {column_name: 'created_at'} + - ['number', 'state', 'payment_state', 'shipment_state', 'email', 'total'].each do |column_name| + %th + = render partial: 'sortable_header', locals: {column_name: column_name} %th.actions %tbody %tr{ng: {repeat: 'order in orders track by $index', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'"} From f743b5f02f6d63dfd829ed91ac2867fd9b08fc17 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 13 Sep 2018 14:45:10 +0200 Subject: [PATCH 118/190] Extract Settings from Product Import processor This encapsulates the data structure used by the entry processor to check various settings. It still requires a lot of work to move more logic to this new class. --- app/models/product_import/entry_processor.rb | 6 ++- app/models/product_import/settings.rb | 13 +++++ spec/models/product_import/settings_spec.rb | 51 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 app/models/product_import/settings.rb create mode 100644 spec/models/product_import/settings_spec.rb diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 315721ffbb..028b973454 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -198,12 +198,14 @@ module ProductImport end def assign_defaults(object, entry) + settings = Settings.new(@import_settings) + # Assigns a default value for a specified field e.g. category='Vegetables', setting this value # either for all entries (overwrite_all), or only for those entries where the field was blank # in the spreadsheet (overwrite_empty), depending on selected import settings - return unless @import_settings.key?(:settings) && @import_settings[:settings][entry.supplier_id.to_s] && @import_settings[:settings][entry.supplier_id.to_s]['defaults'] + return unless settings.defaults(entry) - @import_settings[:settings][entry.supplier_id.to_s]['defaults'].each do |attribute, setting| + settings.defaults(entry).each do |attribute, setting| next unless setting['active'] case setting['mode'] diff --git a/app/models/product_import/settings.rb b/app/models/product_import/settings.rb new file mode 100644 index 0000000000..a97ee544ff --- /dev/null +++ b/app/models/product_import/settings.rb @@ -0,0 +1,13 @@ +module ProductImport + class Settings + def initialize(import_settings) + @import_settings = import_settings + end + + def defaults(entry) + @import_settings.key?(:settings) && + @import_settings[:settings][entry.supplier_id.to_s] && + @import_settings[:settings][entry.supplier_id.to_s]['defaults'] + end + end +end diff --git a/spec/models/product_import/settings_spec.rb b/spec/models/product_import/settings_spec.rb new file mode 100644 index 0000000000..768bc315ad --- /dev/null +++ b/spec/models/product_import/settings_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe ProductImport::Settings do + let(:settings) { described_class.new(import_settings) } + + describe '#defaults' do + let(:entry) { instance_double(ProductImport::SpreadsheetEntry) } + let(:import_settings) { {} } + + context 'when there are no settings' do + it 'returns false' do + expect(settings.defaults(entry)).to be_falsey + end + end + + context 'when there are settings' do + let(:entry) do + instance_double(ProductImport::SpreadsheetEntry, supplier_id: 1) + end + let(:import_settings) { { settings: {} } } + + context 'and there is no data for the specified entry' do + it 'returns a falsey' do + expect(settings.defaults(entry)).to be_falsey + end + end + + context 'and there is data for the specified entry' do + context 'and it has no defaults' do + let(:import_settings) do + { settings: { '1' => { 'foo' => 'bar' } } } + end + + it 'returns a falsey' do + expect(settings.defaults(entry)).to be_falsey + end + end + + context 'and it has defaults' do + let(:import_settings) do + { settings: { '1' => { 'defaults' => 'default value' } } } + end + + it 'returns a truthy' do + expect(settings.defaults(entry)).to eq('default value') + end + end + end + end + end +end From 6f2760cf928b2391201bdd09f899cb17c607aaeb Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 24 Sep 2018 15:54:05 +0100 Subject: [PATCH 119/190] Move translations into their namespace and use '.key' format --- app/views/spree/admin/orders/index.html.haml | 16 +++++++------- config/locales/en.yml | 23 +++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index dec3c2c576..5aa6f161b8 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -1,9 +1,9 @@ - content_for :page_title do - = t(:listing_orders) + = t('.listing_orders') - content_for :page_actions do %li - = button_link_to t(:new_order), new_admin_order_url, :icon => 'icon-plus', :id => 'admin_new_order' + = button_link_to t('.new_order'), new_admin_order_url, icon: 'icon-plus', id: 'admin_new_order' = render partial: 'spree/admin/shared/order_sub_menu' @@ -48,7 +48,7 @@ %div{'ng-if' => 'order.special_instructions'} %br %span.icon-warning-sign{'ofn-with-tip' => "{{::order.special_instructions}}"} - = t(:note) + = t('.note') %td.align-center %span.state{'ng-class' => 'order.state'} {{'order_state.' + order.state | t}} @@ -65,11 +65,11 @@ %td.align-center %span{'ng-bind-html' => '::order.display_total'} %td.actions - %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{::order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t(:edit)} + %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{::order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t('.edit')} %div{'ng-if' => 'order.ready_to_ship'} - %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{::order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t(:ship)} + %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{::order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.ship')} %div{'ng-if' => 'order.capture_path'} - %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t(:capture)} + %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.capture')} .orders-loading{'ng-show' => 'RequestMonitor.loading'} .row @@ -77,10 +77,10 @@ %img.spinner{ src: "/assets/spinning-circles.svg" } .row .small-12.columns.fullwidth.text-center - %span= t(:loading) + %span= t('.loading') %div{'ng-show' => "!RequestMonitor.loading && orders.length > 0" } = render partial: 'admin/shared/angular_pagination' .no-objects-found{'ng-show' => "!RequestMonitor.loading && orders.length == 0"} - = t(:no_orders_found) + = t('.no_orders_found') diff --git a/config/locales/en.yml b/config/locales/en.yml index e0a9863c03..da68db37b9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -613,16 +613,6 @@ en: controls: back_to_my_inventory: Back to my inventory orders: - index: - capture: "Capture" - ship: "Ship" - edit: "Edit" - note: "Note" - first: "First" - last: "Last" - previous: "Previous" - next: "Next" - loading: "Loading" invoice_email_sent: 'Invoice email has been sent' order_email_resent: 'Order email has been resent' bulk_management: @@ -2655,6 +2645,19 @@ See the %{link} to find out more about %{sitename}'s features and to start using index: inherits_properties_checkbox_hint: "Inherit properties from %{supplier}? (unless overridden above)" orders: + index: + listing_orders: "Listing Orders" + new_order: "New Order" + capture: "Capture" + ship: "Ship" + edit: "Edit" + note: "Note" + first: "First" + last: "Last" + previous: "Previous" + next: "Next" + loading: "Loading" + no_orders_found: "No Orders Found" invoice: issued_on: Issued on tax_invoice: TAX INVOICE From 4a7ee9b91e4acfd8abe8a9b52645f83dcf1ba518 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 24 Sep 2018 17:17:10 +0200 Subject: [PATCH 120/190] Remove no longer used rake task --- lib/tasks/users.rake | 82 -------------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 lib/tasks/users.rake diff --git a/lib/tasks/users.rake b/lib/tasks/users.rake deleted file mode 100644 index 0391c2d4ae..0000000000 --- a/lib/tasks/users.rake +++ /dev/null @@ -1,82 +0,0 @@ -require 'csv' - -namespace :openfoodnetwork do - - namespace :dev do - desc 'export users to CSV' - task export_users: :environment do - CSV.open('db/users.csv', 'wb') do |csv| - csv << user_header - users.each do |user| - csv << user_row(user) - end - end - end - - - desc 'import users from CSV' - task import_users: :environment do - ActionMailer::Base.delivery_method = :test - - CSV.foreach('db/users.csv') do |row| - next if row[0] == 'encrypted_password' - - create_user_from row - end - end - - - private - - def users - # Skip some spambot users - Spree::User.all.reject { |u| u.email =~ /example.net/ } - end - - def user_header - ["encrypted_password", "password_salt", "email", "remember_token", "persistence_token", "reset_password_token", "perishable_token", "sign_in_count", "failed_attempts", "last_request_at", "current_sign_in_at", "last_sign_in_at", "current_sign_in_ip", "last_sign_in_ip", "login", "created_at", "updated_at", "authentication_token", "unlock_token", "locked_at", "remember_created_at", "reset_password_sent_at", - - "role_name", - - "ship_address_firstname", "ship_address_lastname", "ship_address_address1", "ship_address_address2", "ship_address_city", "ship_address_zipcode", "ship_address_phone", "ship_address_state", "ship_address_country", "ship_address_created_at", "ship_address_updated_at", "ship_address_company", - - "bill_address_firstname", "bill_address_lastname", "bill_address_address1", "bill_address_address2", "bill_address_city", "bill_address_zipcode", "bill_address_phone", "bill_address_state", "bill_address_country", "bill_address_created_at", "bill_address_updated_at", "bill_address_company",] - end - - def user_row(user) - sa = user.orders.last.andand.ship_address - ba = user.orders.last.andand.bill_address - - [user.encrypted_password, user.password_salt, user.email, user.remember_token, user.persistence_token, user.reset_password_token, user.perishable_token, user.sign_in_count, user.failed_attempts, user.last_request_at, user.current_sign_in_at, user.last_sign_in_at, user.current_sign_in_ip, user.last_sign_in_ip, user.login, user.created_at, user.updated_at, user.authentication_token, user.unlock_token, user.locked_at, user.remember_created_at, user.reset_password_sent_at, - - user.spree_roles.first.andand.name, - - sa.andand.firstname, sa.andand.lastname, sa.andand.address1, sa.andand.address2, sa.andand.city, sa.andand.zipcode, sa.andand.phone, sa.andand.state, sa.andand.country, sa.andand.created_at, sa.andand.updated_at, sa.andand.company, - - ba.andand.firstname, ba.andand.lastname, ba.andand.address1, ba.andand.address2, ba.andand.city, ba.andand.zipcode, ba.andand.phone, ba.andand.state, ba.andand.country, ba.andand.created_at, ba.andand.updated_at, ba.andand.company,] - end - - def create_user_from(row) - user = Spree::User.create!({password: 'changeme123', password_confirmation: 'changeme123', email: row[2], remember_token: row[3], persistence_token: row[4], reset_password_token: row[5], perishable_token: row[6], sign_in_count: row[7], failed_attempts: row[8], last_request_at: row[9], current_sign_in_at: row[10], last_sign_in_at: row[11], current_sign_in_ip: row[12], last_sign_in_ip: row[13], login: row[14], created_at: row[15], updated_at: row[16], authentication_token: row[17], unlock_token: row[18], locked_at: row[19], remember_created_at: row[20], reset_password_sent_at: row[21]}, without_protection: true) - - user.update_column :encrypted_password, row[0] - user.update_column :password_salt, row[1] - - # Safer if we don't make new users into admins - #role = Spree::Role.find_by_name row[24] - #user.spree_roles << role if role - - sa_state = Spree::State.find_by_name row[30] - sa_country = Spree::Country.find_by_name row[31] - sa = Spree::Address.create!({firstname: row[23], lastname: row[24], address1: row[25], address2: row[26], city: row[27], zipcode: row[28], phone: row[29], state: sa_state, country: sa_country, created_at: row[32], updated_at: row[33], company: row[34]}, without_protection: true) - user.update_column :ship_address_id, sa.id - - ba_state = Spree::State.find_by_name row[42] - ba_country = Spree::Country.find_by_name row[43] - ba = Spree::Address.create!({firstname: row[35], lastname: row[36], address1: row[37], address2: row[38], city: row[39], zipcode: row[40], phone: row[41], state: ba_state, country: ba_country, created_at: row[44], updated_at: row[45], company: row[46]}, without_protection: true) - user.update_column :bill_address_id, ba.id - rescue ActiveRecord::RecordInvalid => e - puts "#{row[2]} - #{e.message}" - end - end -end From 1d9243af190cfff109db22d468cd0be59f017b33 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 24 Sep 2018 16:58:43 +0100 Subject: [PATCH 121/190] DRY and clarify serializer and service --- app/serializers/api/admin/order_serializer.rb | 18 ++++++++++++------ app/services/pending_payments.rb | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/serializers/api/admin/order_serializer.rb b/app/serializers/api/admin/order_serializer.rb index 7b7c5018c1..2a579b2910 100644 --- a/app/serializers/api/admin/order_serializer.rb +++ b/app/serializers/api/admin/order_serializer.rb @@ -17,32 +17,32 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer def show_path return '' unless object.id - Spree::Core::Engine.routes_url_helpers.admin_order_path(object) + spree_routes_helper.admin_order_path(object) end def edit_path return '' unless object.id - Spree::Core::Engine.routes_url_helpers.edit_admin_order_path(object) + spree_routes_helper.edit_admin_order_path(object) end def payments_path return '' unless object.payment_state - Spree::Core::Engine.routes_url_helpers.admin_order_payments_path(object) + spree_routes_helper.admin_order_payments_path(object) end def shipments_path return '' unless object.shipment_state - Spree::Core::Engine.routes_url_helpers.admin_order_shipments_path(object) + spree_routes_helper.admin_order_shipments_path(object) end def ship_path - Spree::Core::Engine.routes_url_helpers.fire_admin_order_path(object, e: 'ship') + spree_routes_helper.fire_admin_order_path(object, e: 'ship') end def capture_path payment_due = PendingPayments.new(object) return '' unless object.payment_required? && payment_due.payment_object - Spree::Core::Engine.routes_url_helpers.fire_admin_order_payment_path(object, payment_due.payment_object.id, e: 'capture') + spree_routes_helper.fire_admin_order_payment_path(object, payment_due.payment_object.id, e: 'capture') end def ready_to_ship @@ -68,4 +68,10 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer def completed_at object.completed_at.blank? ? "" : I18n.l(object.completed_at, format: '%B %d, %Y') end + + private + + def spree_routes_helper + Spree::Core::Engine.routes_url_helpers + end end diff --git a/app/services/pending_payments.rb b/app/services/pending_payments.rb index 729af49e47..8a0d660c2d 100644 --- a/app/services/pending_payments.rb +++ b/app/services/pending_payments.rb @@ -6,7 +6,7 @@ class PendingPayments end def payment_object - @order.payments.select{ |p| p if p.state == 'checkout' }.first + @order.payments.select{ |payment| payment if payment.state == 'checkout' }.first end def can_be_captured? From 97775c0bc1af98f5991e3900e2e21af50526e71f Mon Sep 17 00:00:00 2001 From: Transifex-Openfoodnetwork Date: Tue, 25 Sep 2018 06:12:48 +1000 Subject: [PATCH 122/190] Updating translations for config/locales/fr_CA.yml --- config/locales/fr_CA.yml | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/config/locales/fr_CA.yml b/config/locales/fr_CA.yml index 9e459f0fe5..521ce83241 100644 --- a/config/locales/fr_CA.yml +++ b/config/locales/fr_CA.yml @@ -65,6 +65,9 @@ fr_CA: user_passwords: spree_user: updated_not_active: "Votre mot de passe a été mis à jour, mais votre email n'a pas encore été confirmé." + models: + order_cycle: + cloned_order_cycle_name: "Copie de %{order_cycle}" enterprise_mailer: confirmation_instructions: subject: "Confirmez l'adresse email pour %{enterprise}" @@ -72,9 +75,26 @@ fr_CA: subject: "%{enterprise} est maintenant sur %{sitename}" invite_manager: subject: "%{enterprise} vous a invité comme manager" + order_mailer: + cancel_email: + dear_customer: "Cher Acheteur," + instructions: "Votre commande a été ANNULEE. Veuillez en prendre note et conserver pour preuve si besoin cette confirmation." + order_summary_canceled: "Résumé de la commande [ANNULEE]" + subject: "Annulation de Commande" + subtotal: "Sous-total : %{subtotal}" + total: "Total Commande : %{total}" producer_mailer: order_cycle: subject: "Rapport de cycle de vente pour %{producer}" + shipment_mailer: + shipped_email: + dear_customer: "Cher Acheteur," + instructions: "Votre commande a été expédiée" + shipment_summary: "Résumé de l'envoi" + subject: "Notification d'expédition" + thanks: "Merci pour votre commande." + track_information: "Informations de suivi :%{tracking}" + track_link: "Lien de suivi :%{url}" subscription_mailer: placement_summary_email: subject: Un résumé des dernières commandes récemment passées @@ -137,6 +157,7 @@ fr_CA: free_trial: "essai gratuit" plus_tax: "plus taxe" min_bill_turnover_desc: "Quand le chiffre d'affaire dépasse %{mbt_amount}" + more: "Plus" say_no: "Non" say_yes: "Oui" then: puis @@ -380,6 +401,7 @@ fr_CA: producer_signup_page: Page d'inscription Producteur hub_signup_page: Page d'inscription Hub group_signup_page: Page d'inscription Groupe + main_links: Liens du menu principal footer_and_external_links: Pied de page et Liens Externes your_content: Votre contenu user_guide: Guide utilisateur @@ -782,6 +804,14 @@ fr_CA: new: title: Nouvelle entreprise back_link: Revenir à la liste des entreprises + remove_logo: + remove: "Supprimer l'image" + removed_successfully: "Logo supprimé avec succès" + immediate_removal_warning: "Le logo sera supprimé juste après votre confirmation." + remove_promo_image: + remove: "Supprimer l'image" + removed_successfully: "Bannière supprimée avec succès" + immediate_removal_warning: "La bannière sera supprimée juste après votre confirmation." welcome: welcome_title: Bienvenue sur Open Food Network ! welcome_text: 'Vous avez créé avec succès ' @@ -815,7 +845,10 @@ fr_CA: coordinator_fees: add: Ajouter commission coordinateur filters: + search_by_order_cycle_name: "Recherche par nom de Cycle de Vente..." involving: "Concernant" + any_enterprise: "Toutes les entreprises" + any_schedule: "Tous" form: incoming: Produits entrants (pouvant être mis en vente par les hubs) supplier: Fournisseur @@ -1019,6 +1052,11 @@ fr_CA: stripe_connect_fail: Désolé, la connexion de votre compte Stripe a échoué :-( stripe_connect_settings: resource: Configuration de Stripe Connect + api: + enterprise_logo: + destroy_attachment_does_not_exist: "Aucun logo trouvé " + enterprise_promo_image: + destroy_attachment_does_not_exist: "Aucune bannière trouvée " checkout: already_ordered: cart: "panier" @@ -1099,10 +1137,15 @@ fr_CA: ticket_column_unit_price: "Prix unitaire" ticket_column_total_price: "Prix total" menu_1_title: "Boutiques" + menu_1_url: "/shops" menu_2_title: "Carte" + menu_2_url: "/map" menu_3_title: "Producteurs" + menu_3_url: "/producers" menu_4_title: "Groupes" + menu_4_url: "/groups" menu_5_title: "A propos" + menu_5_url: "http://www.openfoodnetwork.org" menu_6_title: "Se connecter" menu_7_title: "Apprendre" logo: "Logo (640x130)" @@ -1120,6 +1163,7 @@ fr_CA: footer_email: "Email" footer_links_md: "Liens" footer_about_url: "A propos URL" + user_guide_link: "Lien vers le guide utilisateur" name: Nom first_name: Prénom last_name: Nom de famille @@ -1202,12 +1246,17 @@ fr_CA: essential_cookies: "Cookies essentiels" essential_cookies_desc: "Les cookies suivants sont nécessaires au fonctionnement du site openfoodfrance.org." essential_cookies_note: "Les cookies contiennent un identifiant unique, mais pas d'autres données. Vos emails et mots de passe par exemple ne sont jamais exposés dans les cookies!" + cookie_domain: "Déposé par!" cookie_session_desc: "Utilisé pour garder en mémoire l'utilisateur d'une page à l'autre lors de la navigation sur le site, ou pour se souvenir des produits dans le panier." cookie_consent_desc: "Utilisé pour se souvenir du consentement de l'utilisation à l'utilisation de cookies." cookie_remember_me_desc: "Utilisé si l'utilisateur a cliqué sur \"se souvenir de moi\" (pour ne pas avoir à se reconnecter à chaque fois). Ce cookie est automatiquement supprimé après 12 jours. Si l'utilisateur souhaite supprimer ce cookie, il n'a qu'à se déconnecter. Si l'utilisateur ne souhaite pas que ce cookie soit installé sur son terminal, il suffit de ne pas cocher la case \"se souvenir de moi\" au moment de la connexion." cookie_openstreemap_desc: "Utilisé par le logiciel de cartographie open source pour assurer qu'il ne reçoit pas trop de requêtes sur un laps de temps déterminé, et éviter ainsi les risques d'abus de leurs services." cookie_stripe_desc: "Utilisé par le terminal de payement en ligne Stripe (proposé aux utilisateurs d'Open Food France) https://stripe.com/fr/cookies-policy/legal. Même si toutes les boutiques n'utilisent pas Stripe, c'est une bonne pratique en matière de sécurité d'appliquer ce cookie sur toutes les pages vues. Stripe construit probablement une image des pages qui ont un quelconque lien avec l'API connectant Open Food France à leur système de paiement pour détecter les comportements anormaux pouvant suggérer un risque de fraude. Donc ce cookie a un rôle qui va au-delà de la simple fourniture d'un système de paiement. Le supprimer pourrait affecter la sécurité du service. Pour en savoir plus sur la politique de confidentialité de Stripe: https://stripe.com/fr/privacy." statistics_cookies: "Cookies d'analyse de navigation" + statistics_cookies_desc: "Ces cookies ne sont pas obligatoires, mais nous permettent de mieux comprendre votre usage de la plateforme, les endroits où vous bloquez, les fonctionnalités qui semblent vous manquer, ou que vous n'utilisez jamais, afin de fournir le service le plus adapté possible aux besoins des utilisateurs." + statistics_cookies_analytics_desc_html: "Pour analyser les données concernant votre usage de la plateforme, nous utilisons Google Analytics, pas vraiment par choix, mais simplement parce que c'était l'outil d'analyse connecté par défaut via Spree, le logiciel e-commerce open source sur lequel nous avons construit. Mais nous espérons pouvoir rapidement migrer vers Matomo (ex anciennement Piwik), outil d'analyse open source compatible RGPD et engagé sur le respect de la vie privée des utilisateurs." + statistics_cookies_matomo_desc_html: "Pour analyser les données concernant votre usage de la plateforme, nous utilisons Matomo (anciennement Piwik), outil d'analyse open source compatible RGPD et engagé sur le respect de la vie privée des utilisateurs." + statistics_cookies_matomo_optout: "Vous ne voulez pas que vos données soient analysées par Matomo ? Nous ne collectons aucune donnée personnelle, et Matomo nous aide à améliorer le service que nous vous offrons, mais nous respectons votre choix :-)" cookie_analytics_utma_desc: "Utilisé pour distinguer les utilisateurs et les sessions. Ce cookie est installé quand la librairie Javascript s'exécute et qu'aucun cookie __utma n'existe déjà. Le cookie est mis à jour à chaque fois que des données sont envoyées à Google Analytics." cookie_analytics_utmt_desc: "Utilisé pour limiter le taux de requêtes." cookie_analytics_utmb_desc: "Utilisé pour distinguer les nouvelles sessions/visites. Ce cookie est installé quand la librairie Javascript s'exécute et qu'aucun cookie __utmb n'existe déjà. Le cookie est mis à jour à chaque fois que des données sont envoyées à Google Analytics. " @@ -1350,6 +1399,7 @@ fr_CA: email_so_edit_true_html: "Vous pouvez effectuer des modifications jusqu'à la fermeture de la période de commande le %{orders_close_at}." email_so_edit_false_html: "Vous pouvez consulter les détails de cette commande à tout moment." email_so_contact_distributor_html: "Pour toute question contactez %{distributor} via %{email}." + email_so_contact_distributor_to_change_order_html: "Cette commande a été automatiquement créée en votre nom. Vous pouvez effectuer des modifications sur cette commande jusqu'à fermeture de la période de commande le%{orders_close_at} en contactant%{distributor} à%{email} ." email_so_confirmation_intro_html: "Votre commande auprès de %{distributor} est maintenant confirmée" email_so_confirmation_explainer_html: "Cette commande a été automatiquement passée pour vous dans le cadre de votre abonnement, et a maintenant été confirmée." email_so_confirmation_details_html: "Voici les détails concernant cette commande auprès de %{distributor}:" @@ -1611,6 +1661,7 @@ fr_CA: error_number: "saisir un nombre" error_email: "saisir une adresse email" error_not_found_in_database: "%{name} n'a pas été trouvé dans la base de donnée" + error_not_primary_producer: "%{name}n'est pas enregistré comme \"producteur\"" error_no_permission_for_enterprise: "\"%{name}\" : vous n'avez pas les droits requis pour gérer les produits de cette entreprise" item_handling_fees: "Frais logistiques (inclus dans le prix affiché)" january: "Janvier" From e93d46e75acc7da4cfd7eb3013fc9100714eabf9 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 25 Sep 2018 09:35:05 +0100 Subject: [PATCH 123/190] Use .find instead of .select().first --- app/services/pending_payments.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/pending_payments.rb b/app/services/pending_payments.rb index 8a0d660c2d..9569500dda 100644 --- a/app/services/pending_payments.rb +++ b/app/services/pending_payments.rb @@ -6,7 +6,7 @@ class PendingPayments end def payment_object - @order.payments.select{ |payment| payment if payment.state == 'checkout' }.first + @order.payments.find{ |payment| payment.state == 'checkout' } end def can_be_captured? From 2dcc8ea4bb5ce4a25daf8083be9818ee76dc32e0 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 25 Sep 2018 10:05:32 +0100 Subject: [PATCH 124/190] Add spec for pending payments service --- .../services/pending_payments_service_spec.rb | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 spec/services/pending_payments_service_spec.rb diff --git a/spec/services/pending_payments_service_spec.rb b/spec/services/pending_payments_service_spec.rb new file mode 100644 index 0000000000..eb3af6007c --- /dev/null +++ b/spec/services/pending_payments_service_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe PendingPayments do + let(:order1) { + create(:order_with_totals_and_distribution, + completed_at: 1.day.ago, state: "complete", + payments: [create(:payment, state: 'checkout')]) + } + let(:order2) { + create(:order_with_totals_and_distribution, + completed_at: 1.day.ago, state: "complete", + payments: [create(:payment, state: 'completed')]) + } + + describe "#can_be_captured?" do + it "responds with a boolean; if an order has payments that can be captured or not" do + expect(PendingPayments.new(order1).can_be_captured?).to be_truthy + expect(PendingPayments.new(order2).can_be_captured?).to_not be_truthy + end + end + + describe "#payment_object" do + it "returns a capturable payment object if there is one present" do + expect(PendingPayments.new(order1).payment_object).to be_a Spree::Payment + expect(PendingPayments.new(order2).payment_object).to be_nil + end + end +end From a23b1b980da5cf2eedd1b3fcab270537f972b63a Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Tue, 25 Sep 2018 12:34:38 +0100 Subject: [PATCH 125/190] =?UTF-8?q?Make=20Web=20engine=20an=20isolated=20n?= =?UTF-8?q?amespace=20engine=20mounted=20on=20/=20(without=20/web=20prefix?= =?UTF-8?q?)=20This=20approach=20is=20better=20to=20separate=20concerns,?= =?UTF-8?q?=20see=20=E2=80=9CIsolated=20Engine=E2=80=9D=20here:=20https://?= =?UTF-8?q?api.rubyonrails.org/v3.2/classes/Rails/Engine.html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cookies_banner/cookies_banner_controller.js.coffee | 2 +- engines/web/config/routes.rb | 8 +++----- engines/web/lib/web/engine.rb | 1 + 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee index d9444fbd78..a35951943a 100644 --- a/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee +++ b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_controller.js.coffee @@ -1,6 +1,6 @@ Darkswarm.controller "CookiesBannerCtrl", ($scope, CookiesBannerService, $http, $window)-> $scope.acceptCookies = -> - $http.post('/web/api/cookies/consent') + $http.post('/api/cookies/consent') CookiesBannerService.close() CookiesBannerService.disable() diff --git a/engines/web/config/routes.rb b/engines/web/config/routes.rb index 26a1a21690..e9143b1c7d 100644 --- a/engines/web/config/routes.rb +++ b/engines/web/config/routes.rb @@ -1,9 +1,7 @@ Web::Engine.routes.draw do - namespace :web do - namespace :api do - scope '/cookies' do - resource :consent, only: [:show, :create, :destroy], controller: "cookies_consent" - end + namespace :api do + scope '/cookies' do + resource :consent, only: [:show, :create, :destroy], controller: "cookies_consent" end end end diff --git a/engines/web/lib/web/engine.rb b/engines/web/lib/web/engine.rb index 11a353a3fd..5285d6726b 100644 --- a/engines/web/lib/web/engine.rb +++ b/engines/web/lib/web/engine.rb @@ -1,4 +1,5 @@ module Web class Engine < ::Rails::Engine + isolate_namespace Web end end From b1c7e6c0916f604572a2fb3712bb469f3f78292d Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Wed, 26 Sep 2018 12:30:45 +0100 Subject: [PATCH 126/190] Fix assets precompilation by including web/all.js and web/all.css in the assets precompilation list in application.rb --- config/application.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/application.rb b/config/application.rb index 33ad7f6011..9723ca795b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -143,6 +143,7 @@ module Openfoodnetwork config.assets.initialize_on_precompile = true config.assets.precompile += ['store/all.css', 'store/all.js', 'store/shop_front.js', 'iehack.js'] config.assets.precompile += ['admin/all.css', 'admin/*.js', 'admin/**/*.js'] + config.assets.precompile += ['web/all.css', 'web/all.js'] config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all.js'] config.assets.precompile += ['mail/all.css'] config.assets.precompile += ['search/all.css', 'search/*.js'] From 921105301ce51aaab18a0693a9087468d33b57db Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Tue, 25 Sep 2018 14:31:43 +0100 Subject: [PATCH 127/190] Move angular-templates route and controller into Web engine The route and controller were in the main app and the views in the engine, with this commit they stay all inside the engine This is done to keep it simple and remove the unnecessary dependency between main app and engine If we use this mechanism in the future for other things in the main app or other engines, we can find a way to extract/abstract this --- app/controllers/angular_templates_controller.rb | 5 ----- config/routes.rb | 2 -- .../app/controllers/web/angular_templates_controller.rb | 9 +++++++++ .../angular_templates/_cookies_policy_entry.html.haml | 0 .../{ => web}/angular_templates/cookies_banner.html.haml | 0 .../{ => web}/angular_templates/cookies_policy.html.haml | 0 engines/web/config/routes.rb | 2 ++ 7 files changed, 11 insertions(+), 7 deletions(-) delete mode 100644 app/controllers/angular_templates_controller.rb create mode 100644 engines/web/app/controllers/web/angular_templates_controller.rb rename engines/web/app/views/{ => web}/angular_templates/_cookies_policy_entry.html.haml (100%) rename engines/web/app/views/{ => web}/angular_templates/cookies_banner.html.haml (100%) rename engines/web/app/views/{ => web}/angular_templates/cookies_policy.html.haml (100%) diff --git a/app/controllers/angular_templates_controller.rb b/app/controllers/angular_templates_controller.rb deleted file mode 100644 index 44d870cc58..0000000000 --- a/app/controllers/angular_templates_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AngularTemplatesController < ApplicationController - def show - render params[:id].to_s, layout: nil - end -end diff --git a/config/routes.rb b/config/routes.rb index 21d2346933..bc3fc303f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -88,8 +88,6 @@ Openfoodnetwork::Application.routes.draw do get '/:id/shop', to: 'enterprises#shop', as: 'enterprise_shop' get "/enterprises/:permalink", to: redirect("/") # Legacy enterprise URL - get "/angular-templates/:id", to: "angular_templates#show", constraints: { name: %r{[\/\w\.]+} } - namespace :api do resources :enterprises do post :update_image, on: :member diff --git a/engines/web/app/controllers/web/angular_templates_controller.rb b/engines/web/app/controllers/web/angular_templates_controller.rb new file mode 100644 index 0000000000..d8670a4664 --- /dev/null +++ b/engines/web/app/controllers/web/angular_templates_controller.rb @@ -0,0 +1,9 @@ +module Web + class AngularTemplatesController < ApplicationController + helper Web::Engine.helpers + + def show + render params[:id].to_s, layout: nil + end + end +end diff --git a/engines/web/app/views/angular_templates/_cookies_policy_entry.html.haml b/engines/web/app/views/web/angular_templates/_cookies_policy_entry.html.haml similarity index 100% rename from engines/web/app/views/angular_templates/_cookies_policy_entry.html.haml rename to engines/web/app/views/web/angular_templates/_cookies_policy_entry.html.haml diff --git a/engines/web/app/views/angular_templates/cookies_banner.html.haml b/engines/web/app/views/web/angular_templates/cookies_banner.html.haml similarity index 100% rename from engines/web/app/views/angular_templates/cookies_banner.html.haml rename to engines/web/app/views/web/angular_templates/cookies_banner.html.haml diff --git a/engines/web/app/views/angular_templates/cookies_policy.html.haml b/engines/web/app/views/web/angular_templates/cookies_policy.html.haml similarity index 100% rename from engines/web/app/views/angular_templates/cookies_policy.html.haml rename to engines/web/app/views/web/angular_templates/cookies_policy.html.haml diff --git a/engines/web/config/routes.rb b/engines/web/config/routes.rb index e9143b1c7d..121f3bdd9a 100644 --- a/engines/web/config/routes.rb +++ b/engines/web/config/routes.rb @@ -4,4 +4,6 @@ Web::Engine.routes.draw do resource :consent, only: [:show, :create, :destroy], controller: "cookies_consent" end end + + get "/angular-templates/:id", to: "angular_templates#show", constraints: { name: %r{[\/\w\.]+} } end From 600c8fcd4c077cb4c4979f0dde52b18875249b0b Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 14 Sep 2018 15:16:09 +1000 Subject: [PATCH 128/190] Send confirmation emails immediately Using deferred methods on the user model breaks delayed jobs when the user is deleted while the job still exists. We could create a proper job referencing a user id for sending these emails instead. But since the user has to wait for the confirmation email anyway, we can send it within the current request. This should be revised if performance becomes an issue. Sending the email directly also has the advantage that we can tell the user if emailing failed. See the following commits. This change impacts a bunch of specs as we now need a working email setup to create unconfirmed users. This commit introduces a custom matcher to unify testing for confirmation emails. --- app/models/spree/user_decorator.rb | 2 -- .../admin/manager_invitations_controller_spec.rb | 4 +++- .../user_confirmations_controller_spec.rb | 5 +++-- spec/factories.rb | 10 ++++++++++ spec/features/admin/enterprise_roles_spec.rb | 1 + spec/features/admin/users_spec.rb | 16 +++++++++------- spec/features/consumer/account/settings_spec.rb | 1 + spec/features/consumer/authentication_spec.rb | 5 ++--- .../features/consumer/confirm_invitation_spec.rb | 1 + spec/models/spree/user_spec.rb | 9 ++++++--- .../matchers/email_confirmation_matchers.rb | 12 ++++++++++++ 11 files changed, 48 insertions(+), 18 deletions(-) create mode 100644 spec/support/matchers/email_confirmation_matchers.rb diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index cf375e7e65..dec0309d2b 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -24,8 +24,6 @@ Spree.user_class.class_eval do # We use the same options as Spree and add :confirmable devise :confirmable, reconfirmable: true - handle_asynchronously :send_confirmation_instructions - handle_asynchronously :send_on_create_confirmation_instructions # TODO: Later versions of devise have a dedicated after_confirmation callback, so use that after_update :welcome_after_confirm, if: lambda { confirmation_token_changed? && confirmation_token.nil? } diff --git a/spec/controllers/admin/manager_invitations_controller_spec.rb b/spec/controllers/admin/manager_invitations_controller_spec.rb index c5e6bf35f8..6a2623220f 100644 --- a/spec/controllers/admin/manager_invitations_controller_spec.rb +++ b/spec/controllers/admin/manager_invitations_controller_spec.rb @@ -25,13 +25,14 @@ module Admin context "signing up a new user" do before do + create(:mail_method) controller.stub spree_current_user: admin end it "creates a new user, sends an invitation email, and returns the user id" do expect do spree_post :create, {email: 'un.registered@email.com', enterprise_id: enterprise.id} - end.to enqueue_job Delayed::PerformableMethod + end.to send_confirmation_instructions new_user = Spree::User.find_by_email('un.registered@email.com') @@ -45,6 +46,7 @@ module Admin describe "with enterprise permissions" do context "as user with proper enterprise permissions" do before do + create(:mail_method) controller.stub spree_current_user: enterprise_owner end diff --git a/spec/controllers/user_confirmations_controller_spec.rb b/spec/controllers/user_confirmations_controller_spec.rb index 678e7ccd86..f8f38883c8 100644 --- a/spec/controllers/user_confirmations_controller_spec.rb +++ b/spec/controllers/user_confirmations_controller_spec.rb @@ -57,6 +57,8 @@ describe UserConfirmationsController, type: :controller do end context "requesting confirmation instructions to be resent" do + before { create(:mail_method) } + it "redirects the user to login" do spree_post :create, { spree_user: { email: unconfirmed_user.email } } expect(response).to redirect_to login_path @@ -66,8 +68,7 @@ describe UserConfirmationsController, type: :controller do it "sends the confirmation email" do expect do spree_post :create, { spree_user: { email: unconfirmed_user.email } } - end.to enqueue_job Delayed::PerformableMethod - expect(Delayed::Job.last.payload_object.method_name).to eq(:send_confirmation_instructions_without_delay) + end.to send_confirmation_instructions end end end diff --git a/spec/factories.rb b/spec/factories.rb index fd4e4e1bf5..c440b54048 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -513,6 +513,16 @@ FactoryBot.modify do confirmation_sent_at '1970-01-01 00:00:00' confirmed_at '1970-01-01 00:00:01' + before(:create) do |user, evaluator| + if evaluator.confirmation_sent_at + if evaluator.confirmed_at + user.skip_confirmation! + else + user.skip_confirmation_notification! + end + end + end + after(:create) do |user| user.spree_roles.clear # Remove admin role end diff --git a/spec/features/admin/enterprise_roles_spec.rb b/spec/features/admin/enterprise_roles_spec.rb index 56db2cb889..a0d075741b 100644 --- a/spec/features/admin/enterprise_roles_spec.rb +++ b/spec/features/admin/enterprise_roles_spec.rb @@ -137,6 +137,7 @@ feature %q{ end it "can invite unregistered users to be managers" do + create(:mail_method) find('a.button.help-modal').click expect(page).to have_css '#invite-manager-modal' diff --git a/spec/features/admin/users_spec.rb b/spec/features/admin/users_spec.rb index 9af920577a..6b2cbc2177 100644 --- a/spec/features/admin/users_spec.rb +++ b/spec/features/admin/users_spec.rb @@ -4,7 +4,10 @@ feature "Managing users" do include AuthenticationWorkflow context "as super-admin" do - before { quick_login_as_admin } + before do + create(:mail_method) + quick_login_as_admin + end describe "creating a user" do it "shows no confirmation message to start with" do @@ -31,12 +34,11 @@ feature "Managing users" do it "displays success" do visit spree.edit_admin_user_path user - # The `a` element doesn't have an href, so we can't use click_link. - find("a", text: "Resend").click - expect(page).to have_text "Resend done" - - # And it's successful. (testing it here for reduced test time) - expect(Delayed::Job.last.payload_object.method_name).to eq :send_confirmation_instructions_without_delay + expect do + # The `a` element doesn't have an href, so we can't use click_link. + find("a", text: "Resend").click + expect(page).to have_text "Resend done" + end.to send_confirmation_instructions end end end diff --git a/spec/features/consumer/account/settings_spec.rb b/spec/features/consumer/account/settings_spec.rb index 3806356375..383d03a4be 100644 --- a/spec/features/consumer/account/settings_spec.rb +++ b/spec/features/consumer/account/settings_spec.rb @@ -7,6 +7,7 @@ feature "Account Settings", js: true do let(:user) { create(:user, email: 'old@email.com') } before do + create(:mail_method) quick_login_as user end diff --git a/spec/features/consumer/authentication_spec.rb b/spec/features/consumer/authentication_spec.rb index b1f8b1be1f..24a03e4d7e 100644 --- a/spec/features/consumer/authentication_spec.rb +++ b/spec/features/consumer/authentication_spec.rb @@ -75,6 +75,7 @@ feature "Authentication", js: true, retry: 3 do end scenario "Signing up successfully" do + create(:mail_method) fill_in "Email", with: "test@foo.com" fill_in "Choose a password", with: "test12345" fill_in "Confirm password", with: "test12345" @@ -82,9 +83,7 @@ feature "Authentication", js: true, retry: 3 do expect do click_signup_button expect(page).to have_content I18n.t('devise.user_registrations.spree_user.signed_up_but_unconfirmed') - end.to enqueue_job Delayed::PerformableMethod - - expect(Delayed::Job.last.payload_object.method_name).to eq(:send_on_create_confirmation_instructions_without_delay) + end.to send_confirmation_instructions end end diff --git a/spec/features/consumer/confirm_invitation_spec.rb b/spec/features/consumer/confirm_invitation_spec.rb index 9fea517638..c42c2ff8f0 100644 --- a/spec/features/consumer/confirm_invitation_spec.rb +++ b/spec/features/consumer/confirm_invitation_spec.rb @@ -8,6 +8,7 @@ feature "Confirm invitation as manager" do let(:user) { Spree::User.create(email: email, unconfirmed_email: email, password: "secret") } before do + create(:mail_method) user.reset_password_token = Devise.friendly_token user.reset_password_sent_at = Time.now.utc user.save! diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 3cd4da93af..db0af97d15 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -72,10 +72,11 @@ describe Spree.user_class do context "#create" do it "should send a confirmation email" do + create(:mail_method) + expect do - create(:user, confirmed_at: nil) - end.to enqueue_job Delayed::PerformableMethod - expect(Delayed::Job.last.payload_object.method_name).to eq(:send_on_create_confirmation_instructions_without_delay) + create(:user, confirmation_sent_at: nil, confirmed_at: nil) + end.to send_confirmation_instructions end context "with the the same email as existing customers" do @@ -96,6 +97,8 @@ describe Spree.user_class do context "confirming email" do it "should send a welcome email" do + create(:mail_method) + expect do create(:user, confirmed_at: nil).confirm! end.to enqueue_job ConfirmSignupJob diff --git a/spec/support/matchers/email_confirmation_matchers.rb b/spec/support/matchers/email_confirmation_matchers.rb new file mode 100644 index 0000000000..8599f820e9 --- /dev/null +++ b/spec/support/matchers/email_confirmation_matchers.rb @@ -0,0 +1,12 @@ +RSpec::Matchers.define :send_confirmation_instructions do + match do |event_proc| + expect(&event_proc).to change { ActionMailer::Base.deliveries.count }.by 1 + + message = ActionMailer::Base.deliveries.last + expect(message.subject).to eq "Please confirm your OFN account" + end + + def supports_block_expectations? + true + end +end From 3ae073dce598e9ff5c3fcd9d8ef240ca2ef4538e Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 14 Sep 2018 15:29:18 +1000 Subject: [PATCH 129/190] Convert specs to RSpec 3.7.1 syntax with Transpec This conversion is done by Transpec 3.3.0 with the following command: transpec spec/controllers/user_registrations_controller_spec.rb * 10 conversions from: obj.should to: expect(obj).to * 7 conversions from: == expected to: eq(expected) For more details: https://github.com/yujinakayama/transpec#supported-conversions --- .../user_registrations_controller_spec.rb | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/controllers/user_registrations_controller_spec.rb b/spec/controllers/user_registrations_controller_spec.rb index 578ff27214..165a35d854 100644 --- a/spec/controllers/user_registrations_controller_spec.rb +++ b/spec/controllers/user_registrations_controller_spec.rb @@ -11,16 +11,16 @@ describe UserRegistrationsController, type: :controller do render_views it "returns errors when registration fails" do xhr :post, :create, spree_user: {}, :use_route => :spree - response.status.should == 401 + expect(response.status).to eq(401) json = JSON.parse(response.body) - json.should == {"email" => ["can't be blank"], "password" => ["can't be blank"]} + expect(json).to eq({"email" => ["can't be blank"], "password" => ["can't be blank"]}) end it "returns 200 when registration succeeds" do xhr :post, :create, spree_user: {email: "test@test.com", password: "testy123", password_confirmation: "testy123"}, :use_route => :spree - response.status.should == 200 + expect(response.status).to eq(200) json = JSON.parse(response.body) - json.should == {"email" => "test@test.com"} + expect(json).to eq({"email" => "test@test.com"}) expect(controller.spree_current_user).to be_nil end end @@ -28,8 +28,8 @@ describe UserRegistrationsController, type: :controller do context "when registration fails" do it "renders new" do spree_post :create, spree_user: {} - response.status.should == 200 - response.should render_template "spree/user_registrations/new" + expect(response.status).to eq(200) + expect(response).to render_template "spree/user_registrations/new" end end @@ -37,8 +37,8 @@ describe UserRegistrationsController, type: :controller do context "when referer is not '/checkout'" do it "redirects to root" do spree_post :create, spree_user: {email: "test@test.com", password: "testy123", password_confirmation: "testy123"}, :use_route => :spree - response.should redirect_to root_path - assigns[:user].email.should == "test@test.com" + expect(response).to redirect_to root_path + expect(assigns[:user].email).to eq("test@test.com") end end @@ -47,8 +47,8 @@ describe UserRegistrationsController, type: :controller do it "redirects to checkout" do spree_post :create, spree_user: {email: "test@test.com", password: "testy123", password_confirmation: "testy123"}, :use_route => :spree - response.should redirect_to checkout_path - assigns[:user].email.should == "test@test.com" + expect(response).to redirect_to checkout_path + expect(assigns[:user].email).to eq("test@test.com") end end end From 17d951f99d1e49ca70c66d9d1157611f6947d875 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 14 Sep 2018 17:48:40 +1000 Subject: [PATCH 130/190] Rescue from any sign-up errors The most common failure would happen when sending the confirmation email triggered by `user.save`. We rescue any errors here and give feedback to the user. This allows for immediate feedback when the user types an email address that is not accepted by our mail server or the email setup is not configured properly. --- .../javascripts/templates/signup.html.haml | 3 +++ .../user_registrations_controller.rb | 24 +++++++++++------ config/locales/en.yml | 1 + .../user_registrations_controller_spec.rb | 27 +++++++++++++++++-- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/templates/signup.html.haml b/app/assets/javascripts/templates/signup.html.haml index f2d480c8b9..51aa9d7fbc 100644 --- a/app/assets/javascripts/templates/signup.html.haml +++ b/app/assets/javascripts/templates/signup.html.haml @@ -4,6 +4,9 @@ .large-12.columns .alert-box.success{ng: {show: 'messages != null'}} {{ messages }} + .large-12.columns + .alert-box.alert{ng: {show: 'errors.message != null'}} + {{ errors.message }} .row .large-12.columns %label{for: "email"} {{'signup_email' | t}} diff --git a/app/controllers/user_registrations_controller.rb b/app/controllers/user_registrations_controller.rb index b7001ab78b..4827a9a0b0 100644 --- a/app/controllers/user_registrations_controller.rb +++ b/app/controllers/user_registrations_controller.rb @@ -19,14 +19,22 @@ class UserRegistrationsController < Spree::UserRegistrationsController end end else - clean_up_passwords(resource) - respond_to do |format| - format.html do - render :new - end - format.js do - render json: @user.errors, status: :unauthorized - end + render_error(@user.errors) + end + rescue StandardError + render_error(message: I18n.t('devise.user_registrations.spree_user.unknown_error')) + end + + private + + def render_error(errors = {}) + clean_up_passwords(resource) + respond_to do |format| + format.html do + render :new + end + format.js do + render json: errors, status: :unauthorized end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 6f6550befa..a289a1e4b6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -79,6 +79,7 @@ en: user_registrations: spree_user: signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." + unknown_error: "Something went wrong while creating your account. Check your email address and try again." failure: invalid: | Invalid email or password. diff --git a/spec/controllers/user_registrations_controller_spec.rb b/spec/controllers/user_registrations_controller_spec.rb index 165a35d854..505a1846e2 100644 --- a/spec/controllers/user_registrations_controller_spec.rb +++ b/spec/controllers/user_registrations_controller_spec.rb @@ -3,21 +3,44 @@ require 'spree/api/testing_support/helpers' describe UserRegistrationsController, type: :controller do + before(:all) do + create(:mail_method) + end + before do @request.env["devise.mapping"] = Devise.mappings[:spree_user] end describe "via ajax" do render_views - it "returns errors when registration fails" do + + let(:user_params) do + { + email: "test@test.com", + password: "testy123", + password_confirmation: "testy123" + } + end + + it "returns validation errors" do xhr :post, :create, spree_user: {}, :use_route => :spree expect(response.status).to eq(401) json = JSON.parse(response.body) expect(json).to eq({"email" => ["can't be blank"], "password" => ["can't be blank"]}) end + it "returns error when emailing fails" do + allow(Spree::UserMailer).to receive(:confirmation_instructions).and_raise("Some error") + + xhr :post, :create, spree_user: user_params, use_route: :spree + + expect(response.status).to eq(401) + json = JSON.parse(response.body) + expect(json).to eq({"message" => I18n.t('devise.user_registrations.spree_user.unknown_error')}) + end + it "returns 200 when registration succeeds" do - xhr :post, :create, spree_user: {email: "test@test.com", password: "testy123", password_confirmation: "testy123"}, :use_route => :spree + xhr :post, :create, spree_user: user_params, use_route: :spree expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json).to eq({"email" => "test@test.com"}) From 9dcc683dc03428a03c68cc63fd6c9994c10c77f7 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Mon, 17 Sep 2018 10:50:24 +1000 Subject: [PATCH 131/190] Notify Bugsnag on sign-up errors This may lead to more error reports than we want to see. A not existing email address may cause Bugsnag to be notified. If this happens, we can rescue form these specific errors and only report the rest. --- app/controllers/user_registrations_controller.rb | 5 ++++- lib/open_food_network/error_logger.rb | 13 +++++++++++++ .../user_registrations_controller_spec.rb | 1 + spec/lib/open_food_network/error_logger_spec.rb | 14 ++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 lib/open_food_network/error_logger.rb create mode 100644 spec/lib/open_food_network/error_logger_spec.rb diff --git a/app/controllers/user_registrations_controller.rb b/app/controllers/user_registrations_controller.rb index 4827a9a0b0..654fe1791e 100644 --- a/app/controllers/user_registrations_controller.rb +++ b/app/controllers/user_registrations_controller.rb @@ -1,3 +1,5 @@ +require 'open_food_network/error_logger' + class UserRegistrationsController < Spree::UserRegistrationsController before_filter :set_checkout_redirect, only: :create @@ -21,7 +23,8 @@ class UserRegistrationsController < Spree::UserRegistrationsController else render_error(@user.errors) end - rescue StandardError + rescue StandardError => error + OpenFoodNetwork::ErrorLogger.notify(error) render_error(message: I18n.t('devise.user_registrations.spree_user.unknown_error')) end diff --git a/lib/open_food_network/error_logger.rb b/lib/open_food_network/error_logger.rb new file mode 100644 index 0000000000..a1199c9e68 --- /dev/null +++ b/lib/open_food_network/error_logger.rb @@ -0,0 +1,13 @@ +# Our error logging API currently wraps Bugsnag. +# It makes us more flexible if we wanted to replace Bugsnag or change logging +# behaviour. +module OpenFoodNetwork + module ErrorLogger + # Tries to escalate the error to a developer. + # If Bugsnag is configured, it will notify it. It would be nice to implement + # some kind of fallback. + def self.notify(error) + Bugsnag.notify(error) + end + end +end diff --git a/spec/controllers/user_registrations_controller_spec.rb b/spec/controllers/user_registrations_controller_spec.rb index 505a1846e2..cb9d278774 100644 --- a/spec/controllers/user_registrations_controller_spec.rb +++ b/spec/controllers/user_registrations_controller_spec.rb @@ -31,6 +31,7 @@ describe UserRegistrationsController, type: :controller do it "returns error when emailing fails" do allow(Spree::UserMailer).to receive(:confirmation_instructions).and_raise("Some error") + expect(OpenFoodNetwork::ErrorLogger).to receive(:notify) xhr :post, :create, spree_user: user_params, use_route: :spree diff --git a/spec/lib/open_food_network/error_logger_spec.rb b/spec/lib/open_food_network/error_logger_spec.rb new file mode 100644 index 0000000000..f1cd326fa2 --- /dev/null +++ b/spec/lib/open_food_network/error_logger_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' +require 'open_food_network/error_logger' + +module OpenFoodNetwork + describe ErrorLogger do + let(:error) { StandardError.new("Test") } + + it "notifies Bugsnag" do + expect(Bugsnag).to receive(:notify).with(error) + + ErrorLogger.notify(error) + end + end +end From af1ac333df533960288dd8f4f54b7eff44e31759 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Mon, 17 Sep 2018 15:31:42 +1000 Subject: [PATCH 132/190] Create MailMethod before User when seeding --- db/seeds.rb | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index d285cbda01..ddc3d76374 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -2,6 +2,29 @@ # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). require 'yaml' +def create_mail_method + Spree::MailMethod.destroy_all + + CreateMailMethod.new( + environment: Rails.env, + preferred_enable_mail_delivery: true, + preferred_mail_host: ENV.fetch('MAIL_HOST'), + preferred_mail_domain: ENV.fetch('MAIL_DOMAIN'), + preferred_mail_port: ENV.fetch('MAIL_PORT'), + preferred_mail_auth_type: 'login', + preferred_smtp_username: ENV.fetch('SMTP_USERNAME'), + preferred_smtp_password: ENV.fetch('SMTP_PASSWORD'), + preferred_secure_connection_type: ENV.fetch('MAIL_SECURE_CONNECTION', 'None'), + preferred_mails_from: ENV.fetch('MAILS_FROM', "no-reply@#{ENV.fetch('MAIL_DOMAIN')}"), + preferred_mail_bcc: ENV.fetch('MAIL_BCC', ''), + preferred_intercept_email: '' + ).call +end + +# We need a mail method to create a user account, because it sends a +# confirmation email. +create_mail_method + # -- Spree unless Spree::Country.find_by_iso(ENV['DEFAULT_COUNTRY_CODE']) puts "[db:seed] Seeding Spree" @@ -30,26 +53,5 @@ states.each do |state| end end -def create_mail_method - Spree::MailMethod.destroy_all - - CreateMailMethod.new( - environment: Rails.env, - preferred_enable_mail_delivery: true, - preferred_mail_host: ENV.fetch('MAIL_HOST'), - preferred_mail_domain: ENV.fetch('MAIL_DOMAIN'), - preferred_mail_port: ENV.fetch('MAIL_PORT'), - preferred_mail_auth_type: 'login', - preferred_smtp_username: ENV.fetch('SMTP_USERNAME'), - preferred_smtp_password: ENV.fetch('SMTP_PASSWORD'), - preferred_secure_connection_type: ENV.fetch('MAIL_SECURE_CONNECTION', 'None'), - preferred_mails_from: ENV.fetch('MAILS_FROM', "no-reply@#{ENV.fetch('MAIL_DOMAIN')}"), - preferred_mail_bcc: ENV.fetch('MAIL_BCC', ''), - preferred_intercept_email: '' - ).call -end - -create_mail_method - spree_user = Spree::User.first spree_user && spree_user.confirm! From f0021be53c18cfc1193942e2750c4c095beb2313 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 27 Sep 2018 11:34:09 +1000 Subject: [PATCH 133/190] Style I18n call --- app/controllers/user_registrations_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/user_registrations_controller.rb b/app/controllers/user_registrations_controller.rb index 654fe1791e..cc43badbb8 100644 --- a/app/controllers/user_registrations_controller.rb +++ b/app/controllers/user_registrations_controller.rb @@ -1,6 +1,8 @@ require 'open_food_network/error_logger' class UserRegistrationsController < Spree::UserRegistrationsController + I18N_SCOPE = 'devise.user_registrations.spree_user'.freeze + before_filter :set_checkout_redirect, only: :create # POST /resource/sign_up @@ -25,7 +27,7 @@ class UserRegistrationsController < Spree::UserRegistrationsController end rescue StandardError => error OpenFoodNetwork::ErrorLogger.notify(error) - render_error(message: I18n.t('devise.user_registrations.spree_user.unknown_error')) + render_error(message: I18n.t('unknown_error', scope: I18N_SCOPE)) end private From 54b17ac7018449960a6c959ef551d5b7c5b7fbd7 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Thu, 27 Sep 2018 13:04:56 +0100 Subject: [PATCH 134/190] Use Spree::Order.pending_payments and remove service --- app/serializers/api/admin/order_serializer.rb | 6 ++-- app/services/pending_payments.rb | 15 ---------- .../services/pending_payments_service_spec.rb | 28 ------------------- 3 files changed, 3 insertions(+), 46 deletions(-) delete mode 100644 app/services/pending_payments.rb delete mode 100644 spec/services/pending_payments_service_spec.rb diff --git a/app/serializers/api/admin/order_serializer.rb b/app/serializers/api/admin/order_serializer.rb index 2a579b2910..7ea181da5d 100644 --- a/app/serializers/api/admin/order_serializer.rb +++ b/app/serializers/api/admin/order_serializer.rb @@ -40,9 +40,9 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer end def capture_path - payment_due = PendingPayments.new(object) - return '' unless object.payment_required? && payment_due.payment_object - spree_routes_helper.fire_admin_order_payment_path(object, payment_due.payment_object.id, e: 'capture') + pending_payment = object.pending_payments.first + return '' unless object.payment_required? && pending_payment + spree_routes_helper.fire_admin_order_payment_path(object, pending_payment.id, e: 'capture') end def ready_to_ship diff --git a/app/services/pending_payments.rb b/app/services/pending_payments.rb deleted file mode 100644 index 9569500dda..0000000000 --- a/app/services/pending_payments.rb +++ /dev/null @@ -1,15 +0,0 @@ -# Returns the capturable payment object for an order with balance due - -class PendingPayments - def initialize(order) - @order = order - end - - def payment_object - @order.payments.find{ |payment| payment.state == 'checkout' } - end - - def can_be_captured? - payment_object && payment_object.actions.include?('capture') - end -end diff --git a/spec/services/pending_payments_service_spec.rb b/spec/services/pending_payments_service_spec.rb deleted file mode 100644 index eb3af6007c..0000000000 --- a/spec/services/pending_payments_service_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe PendingPayments do - let(:order1) { - create(:order_with_totals_and_distribution, - completed_at: 1.day.ago, state: "complete", - payments: [create(:payment, state: 'checkout')]) - } - let(:order2) { - create(:order_with_totals_and_distribution, - completed_at: 1.day.ago, state: "complete", - payments: [create(:payment, state: 'completed')]) - } - - describe "#can_be_captured?" do - it "responds with a boolean; if an order has payments that can be captured or not" do - expect(PendingPayments.new(order1).can_be_captured?).to be_truthy - expect(PendingPayments.new(order2).can_be_captured?).to_not be_truthy - end - end - - describe "#payment_object" do - it "returns a capturable payment object if there is one present" do - expect(PendingPayments.new(order1).payment_object).to be_a Spree::Payment - expect(PendingPayments.new(order2).payment_object).to be_nil - end - end -end From 16badcd1b546d493807b776b647ec8c6ba4d07a3 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 27 Sep 2018 15:40:35 +0200 Subject: [PATCH 135/190] Setup Simplecov to have code coverage analysis This enables code coverage analysis when running specs in your dev environment. Simply run them as usual and you'll see a line like the following at the end of the output: Coverage report generated for RSpec to /home/pau/dev/openfoodnetwork/coverage Simply browse to coverage/index.html and the results in a web UI. This is a useful tool that helps you decide if the tests you added are enough or not. --- .gitignore | 1 + Gemfile | 1 + Gemfile.lock | 7 +++++++ spec/spec_helper.rb | 3 +++ 4 files changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index 060eb24f85..4c0d217d06 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ libpeerconnection.log /config/application.yml node_modules vendor/bundle/ +coverage diff --git a/Gemfile b/Gemfile index 2b33c60993..9473e1b32b 100644 --- a/Gemfile +++ b/Gemfile @@ -129,6 +129,7 @@ end group :test do gem 'webmock' + gem 'simplecov', require: false # See spec/spec_helper.rb for instructions #gem 'perftools.rb' end diff --git a/Gemfile.lock b/Gemfile.lock index a63804e6bf..7866190d4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -265,6 +265,7 @@ GEM devise (>= 2.1.0) diff-lcs (1.3) diffy (3.1.0) + docile (1.3.1) dry-inflector (0.1.2) em-websocket (0.5.1) eventmachine (>= 0.12.9) @@ -688,6 +689,11 @@ GEM shellany (0.0.1) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) + simplecov (0.16.1) + docile (~> 1.1) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.2) skylight (1.6.1) activesupport (>= 3.0.0) spinjs-rails (1.3) @@ -832,6 +838,7 @@ DEPENDENCIES sass-rails (~> 3.2.3) shoulda-matchers simple_form! + simplecov skylight (< 2.0) spinjs-rails spree! diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 40dc6cd256..328c2aaac3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,6 @@ +require 'simplecov' +SimpleCov.start 'rails' + require 'rubygems' # Require pry when we're not inside Travis-CI From 3cf10020bf8543b053974905ddfb3a2dc781195f Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Thu, 27 Sep 2018 17:47:36 +0100 Subject: [PATCH 136/190] Fix bug on slow page loads where banner and policy modal were both loaded This solution makes the banner aware of the policy modal: the banner doesnt open if the policy modal is enabled --- .../cookies_banner_directive.js.coffee | 3 ++- .../cookies_policy_modal_service.js.coffee | 16 +++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_directive.js.coffee b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_directive.js.coffee index 0599646cb7..a00354774c 100644 --- a/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_directive.js.coffee +++ b/engines/web/app/assets/javascripts/web/cookies_banner/cookies_banner_directive.js.coffee @@ -1,6 +1,7 @@ -Darkswarm.directive 'cookiesBanner', (CookiesBannerService) -> +Darkswarm.directive 'cookiesBanner', (CookiesBannerService, CookiesPolicyModalService) -> restrict: 'A' link: (scope, elm, attr)-> return if not attr.cookiesBanner? || attr.cookiesBanner == 'false' CookiesBannerService.enable() + return if CookiesPolicyModalService.isEnabled() CookiesBannerService.open() diff --git a/engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_service.js.coffee b/engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_service.js.coffee index 0f91645cfd..a988b1eb0f 100644 --- a/engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_service.js.coffee +++ b/engines/web/app/assets/javascripts/web/cookies_policy/cookies_policy_modal_service.js.coffee @@ -5,7 +5,7 @@ Darkswarm.factory "CookiesPolicyModalService", (Navigation, $modal, $location, C modalMessage: null constructor: -> - if $location.path() is @defaultPath || location.pathname is @defaultPath + if @isEnabled() @open '' open: (path = false, template = 'angular-templates/cookies_policy.html') => @@ -13,18 +13,16 @@ Darkswarm.factory "CookiesPolicyModalService", (Navigation, $modal, $location, C templateUrl: template windowClass: "cookies-policy-modal medium" - @closeCookiesBanner() - @onCloseReOpenCookiesBanner() + CookiesBannerService.close() + @onCloseOpenCookiesBanner() selectedPath = path || @defaultPath Navigation.navigate selectedPath - closeCookiesBanner: => - setTimeout -> - CookiesBannerService.close() - , 200 - - onCloseReOpenCookiesBanner: => + onCloseOpenCookiesBanner: => @modalInstance.result.then( -> CookiesBannerService.open(), -> CookiesBannerService.open() ) + + isEnabled: => + $location.path() is @defaultPath || location.pathname is @defaultPath From 3150741849e375ef3a78dfb611e1df47f4698c21 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Fri, 21 Sep 2018 18:47:43 +0200 Subject: [PATCH 137/190] Extract ResetAbsent class from EntryProcessor --- app/models/product_import/entry_processor.rb | 55 +++--------- app/models/product_import/reset_absent.rb | 53 ++++++++++++ .../product_import/entry_processor_spec.rb | 40 +++++++++ .../product_import/reset_absent_spec.rb | 86 +++++++++++++++++++ 4 files changed, 191 insertions(+), 43 deletions(-) create mode 100644 app/models/product_import/reset_absent.rb create mode 100644 spec/models/product_import/entry_processor_spec.rb create mode 100644 spec/models/product_import/reset_absent_spec.rb diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 028b973454..323b4859b8 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -4,7 +4,7 @@ module ProductImport class EntryProcessor - attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :products_reset_count, :supplier_products, :total_supplier_products + attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :products_reset_count, :supplier_products, :total_supplier_products, :import_settings def initialize(importer, validator, import_settings, spreadsheet_data, editable_enterprises, import_time, updated_ids) @importer = importer @@ -60,46 +60,23 @@ module ProductImport end def reset_absent_items - # For selected enterprises; set stock to zero for all products/inventory - # that were not listed in the newly uploaded spreadsheet - return unless data_for_stock_reset? - suppliers_to_reset_products = [] - suppliers_to_reset_inventories = [] - - settings = @import_settings[:settings] - - @import_settings[:enterprises_to_reset].each do |enterprise_id| - suppliers_to_reset_products.push Integer(enterprise_id) if settings['reset_all_absent'] && permission_by_id?(enterprise_id) && !importing_into_inventory? - suppliers_to_reset_inventories.push Integer(enterprise_id) if settings['reset_all_absent'] && permission_by_id?(enterprise_id) && importing_into_inventory? - end - - unless suppliers_to_reset_inventories.empty? - @products_reset_count += VariantOverride. - where('variant_overrides.hub_id IN (?) - AND variant_overrides.id NOT IN (?)', suppliers_to_reset_inventories, @import_settings[:updated_ids]). - update_all(count_on_hand: 0) - end - - return if suppliers_to_reset_products.empty? - - @products_reset_count += Spree::Variant.joins(:product). - where('spree_products.supplier_id IN (?) - AND spree_variants.id NOT IN (?) - AND spree_variants.is_master = false - AND spree_variants.deleted_at IS NULL', suppliers_to_reset_products, @import_settings[:updated_ids]). - update_all(count_on_hand: 0) + ResetAbsent.new(self).call end def total_saved_count @products_created + @variants_created + @variants_updated + @inventory_created + @inventory_updated end - private - - def data_for_stock_reset? - @import_settings[:settings] && @import_settings[:updated_ids] && @import_settings[:enterprises_to_reset] + def permission_by_id?(supplier_id) + @editable_enterprises.value?(Integer(supplier_id)) end + def importing_into_inventory? + import_settings[:settings] && import_settings[:settings]['import_into'] == 'inventories' + end + + private + def save_to_inventory(entry) save_new_inventory_item entry if entry.validates_as? 'new_inventory_item' save_existing_inventory_item entry if entry.validates_as? 'existing_inventory_item' @@ -126,7 +103,7 @@ module ProductImport end def import_into_inventory?(entry) - entry.supplier_id && @import_settings[:settings]['import_into'] == 'inventories' + entry.supplier_id && import_settings[:settings]['import_into'] == 'inventories' end def save_new_inventory_item(entry) @@ -198,7 +175,7 @@ module ProductImport end def assign_defaults(object, entry) - settings = Settings.new(@import_settings) + settings = Settings.new(import_settings) # Assigns a default value for a specified field e.g. category='Vegetables', setting this value # either for all entries (overwrite_all), or only for those entries where the field was blank @@ -241,13 +218,5 @@ module ProductImport variant.import_date = @import_time variant.save end - - def permission_by_id?(supplier_id) - @editable_enterprises.value?(Integer(supplier_id)) - end - - def importing_into_inventory? - @import_settings[:settings] && @import_settings[:settings]['import_into'] == 'inventories' - end end end diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb new file mode 100644 index 0000000000..aad5ed4de9 --- /dev/null +++ b/app/models/product_import/reset_absent.rb @@ -0,0 +1,53 @@ +require 'delegate' + +module ProductImport + class ResetAbsent < SimpleDelegator + def call + # For selected enterprises; set stock to zero for all products/inventory + # that were not listed in the newly uploaded spreadsheet + return unless data_for_stock_reset? + suppliers_to_reset_products = [] + suppliers_to_reset_inventories = [] + + settings = import_settings[:settings] + + import_settings[:enterprises_to_reset].each do |enterprise_id| + if settings['reset_all_absent'] && + permission_by_id?(enterprise_id) && + !importing_into_inventory? + suppliers_to_reset_products.push(Integer(enterprise_id)) + end + + if settings['reset_all_absent'] && + permission_by_id?(enterprise_id) && + importing_into_inventory? + suppliers_to_reset_inventories.push(Integer(enterprise_id)) + end + end + + unless suppliers_to_reset_inventories.empty? + @products_reset_count += VariantOverride. + where('variant_overrides.hub_id IN (?) + AND variant_overrides.id NOT IN (?)', suppliers_to_reset_inventories, import_settings[:updated_ids]). + update_all(count_on_hand: 0) + end + + return if suppliers_to_reset_products.empty? + + @products_reset_count += Spree::Variant.joins(:product). + where('spree_products.supplier_id IN (?) + AND spree_variants.id NOT IN (?) + AND spree_variants.is_master = false + AND spree_variants.deleted_at IS NULL', suppliers_to_reset_products, import_settings[:updated_ids]). + update_all(count_on_hand: 0) + end + + private + + def data_for_stock_reset? + import_settings[:settings] && + import_settings[:updated_ids] && + import_settings[:enterprises_to_reset] + end + end +end diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb new file mode 100644 index 0000000000..866bf742b8 --- /dev/null +++ b/spec/models/product_import/entry_processor_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe ProductImport::EntryProcessor do + let(:importer) { double(:importer) } + let(:validator) { double(:validator) } + let(:import_settings) { double(:import_settings) } + let(:spreadsheet_data) { double(:spreadsheet_data) } + let(:editable_enterprises) { double(:editable_enterprises) } + let(:import_time) { double(:import_time) } + let(:updated_ids) { double(:updated_ids) } + + let(:entry_processor) do + described_class.new( + importer, + validator, + import_settings, + spreadsheet_data, + editable_enterprises, + import_time, + updated_ids + ) + end + + describe '#reset_absent_items' do + let(:reset_absent) { double(ProductImport::ResetAbsent, call: true) } + + before do + allow(ProductImport::ResetAbsent) + .to receive(:new) + .and_return(reset_absent) + end + + it 'delegates to ResetAbsent' do + entry_processor.reset_absent_items + + expect(ProductImport::ResetAbsent) + .to have_received(:new).with(entry_processor) + end + end +end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb new file mode 100644 index 0000000000..2be1df43c7 --- /dev/null +++ b/spec/models/product_import/reset_absent_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe ProductImport::ResetAbsent do + let(:importer) { double(:importer) } + let(:validator) { double(:validator) } + let(:spreadsheet_data) { double(:spreadsheet_data) } + let(:editable_enterprises) { double(:editable_enterprises) } + let(:import_time) { double(:import_time) } + let(:updated_ids) { double(:updated_ids) } + + let(:entry_processor) do + ProductImport::EntryProcessor.new( + importer, + validator, + import_settings, + spreadsheet_data, + editable_enterprises, + import_time, + updated_ids + ) + end + + let(:reset_absent) { described_class.new(entry_processor) } + + describe '#call' do + context 'when there are no settings' do + let(:import_settings) { { updated_ids: [], enterprises_to_reset: [] } } + + it 'returns nil' do + expect(reset_absent.call).to be_nil + end + end + + context 'when there are no updated_ids' do + let(:import_settings) { { settings: [], enterprises_to_reset: [] } } + + it 'returns nil' do + expect(reset_absent.call).to be_nil + end + end + + context 'when there are no enterprises_to_reset' do + let(:import_settings) { { settings: [], updated_ids: [] } } + + it 'returns nil' do + expect(reset_absent.call).to be_nil + end + end + + context 'when there are settings, updated_ids and enterprises_to_reset' do + let(:import_settings) do + { + settings: { 'reset_all_absent' => true }, + updated_ids: [1], + enterprises_to_reset: [2] + } + end + + before do + allow(entry_processor).to receive(:permission_by_id?).with(2) { true } + end + + context 'and not importing into inventory' do + before do + allow(entry_processor) + .to receive(:importing_into_inventory?) { false } + end + + it 'returns true' do + expect(reset_absent.call).to eq(true) + end + end + + context 'and importing into inventory' do + before do + allow(entry_processor) + .to receive(:importing_into_inventory?) { true } + end + + it 'returns true' do + expect(reset_absent.call).to eq(true) + end + end + end + end +end From 663db47433a9261aa97fcfeae61a7b7ed78b05cd Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 24 Sep 2018 13:49:27 +0200 Subject: [PATCH 138/190] Move products_reset_count to ResetAbsent --- app/models/product_import/entry_processor.rb | 11 ++-- app/models/product_import/reset_absent.rb | 36 +++++++++---- .../product_import/entry_processor_spec.rb | 19 ++++++- .../product_import/reset_absent_spec.rb | 54 ++++++++++++++++--- 4 files changed, 99 insertions(+), 21 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 323b4859b8..a6ace4d659 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -4,7 +4,9 @@ module ProductImport class EntryProcessor - attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :products_reset_count, :supplier_products, :total_supplier_products, :import_settings + delegate :products_reset_count, to: :reset_absent + + attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :supplier_products, :total_supplier_products, :import_settings def initialize(importer, validator, import_settings, spreadsheet_data, editable_enterprises, import_time, updated_ids) @importer = importer @@ -20,7 +22,6 @@ module ProductImport @products_created = 0 @variants_created = 0 @variants_updated = 0 - @products_reset_count = 0 @supplier_products = {} @total_supplier_products = 0 end @@ -60,7 +61,11 @@ module ProductImport end def reset_absent_items - ResetAbsent.new(self).call + reset_absent.call + end + + def reset_absent + @reset_absent ||= ResetAbsent.new(self) end def total_saved_count diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index aad5ed4de9..439945bb06 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -2,6 +2,13 @@ require 'delegate' module ProductImport class ResetAbsent < SimpleDelegator + attr_reader :products_reset_count + + def initialize(decorated) + super + @products_reset_count = 0 + end + def call # For selected enterprises; set stock to zero for all products/inventory # that were not listed in the newly uploaded spreadsheet @@ -26,20 +33,29 @@ module ProductImport end unless suppliers_to_reset_inventories.empty? - @products_reset_count += VariantOverride. - where('variant_overrides.hub_id IN (?) - AND variant_overrides.id NOT IN (?)', suppliers_to_reset_inventories, import_settings[:updated_ids]). - update_all(count_on_hand: 0) + relation = VariantOverride + .where( + 'variant_overrides.hub_id IN (?) ' \ + 'AND variant_overrides.id NOT IN (?)', + suppliers_to_reset_inventories, + import_settings[:updated_ids] + ) + @products_reset_count += relation.update_all(count_on_hand: 0) end return if suppliers_to_reset_products.empty? - @products_reset_count += Spree::Variant.joins(:product). - where('spree_products.supplier_id IN (?) - AND spree_variants.id NOT IN (?) - AND spree_variants.is_master = false - AND spree_variants.deleted_at IS NULL', suppliers_to_reset_products, import_settings[:updated_ids]). - update_all(count_on_hand: 0) + relation = Spree::Variant + .joins(:product) + .where( + 'spree_products.supplier_id IN (?) ' \ + 'AND spree_variants.id NOT IN (?) ' \ + 'AND spree_variants.is_master = false ' \ + 'AND spree_variants.deleted_at IS NULL', + suppliers_to_reset_products, + import_settings[:updated_ids] + ) + @products_reset_count += relation.update_all(count_on_hand: 0) end private diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index 866bf742b8..88205a85d3 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -22,7 +22,7 @@ describe ProductImport::EntryProcessor do end describe '#reset_absent_items' do - let(:reset_absent) { double(ProductImport::ResetAbsent, call: true) } + let(:reset_absent) { instance_double(ProductImport::ResetAbsent, call: true) } before do allow(ProductImport::ResetAbsent) @@ -37,4 +37,21 @@ describe ProductImport::EntryProcessor do .to have_received(:new).with(entry_processor) end end + + describe '#products_reset_count' do + let(:reset_absent) { instance_double(ProductImport::ResetAbsent) } + + before do + allow(ProductImport::ResetAbsent) + .to receive(:new) + .and_return(reset_absent) + + allow(reset_absent).to receive(:products_reset_count) + end + + it 'delegates to ResetAbsent' do + entry_processor.products_reset_count + expect(reset_absent).to have_received(:products_reset_count) + end + end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 2be1df43c7..f2addc9dc3 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -51,36 +51,76 @@ describe ProductImport::ResetAbsent do let(:import_settings) do { settings: { 'reset_all_absent' => true }, - updated_ids: [1], - enterprises_to_reset: [2] + updated_ids: [0], + enterprises_to_reset: [enterprise.id] } end before do - allow(entry_processor).to receive(:permission_by_id?).with(2) { true } + allow(entry_processor) + .to receive(:permission_by_id?).with(enterprise.id) { true } end context 'and not importing into inventory' do + let(:variant) { create(:variant) } + let(:enterprise) { variant.product.supplier } + before do allow(entry_processor) .to receive(:importing_into_inventory?) { false } end - it 'returns true' do - expect(reset_absent.call).to eq(true) + it 'returns the number of products reset' do + expect(reset_absent.call).to eq(2) end end context 'and importing into inventory' do + let(:variant) { create(:variant) } + let(:enterprise) { variant.product.supplier } + let(:variant_override) do + create(:variant_override, variant: variant, hub: enterprise) + end + + before do + variant_override + + allow(entry_processor) + .to receive(:permission_by_id?).with(enterprise.id) { true } + end + before do allow(entry_processor) .to receive(:importing_into_inventory?) { true } end - it 'returns true' do - expect(reset_absent.call).to eq(true) + it 'returns nil' do + expect(reset_absent.call).to be_nil end end end end + + describe '#products_reset_count' do + let(:variant) { create(:variant) } + let(:enterprise_id) { variant.product.supplier_id } + + before do + allow(entry_processor) + .to receive(:permission_by_id?).with(enterprise_id) { true } + end + + let(:import_settings) do + { + settings: { 'reset_all_absent' => true }, + updated_ids: [0], + enterprises_to_reset: [enterprise_id] + } + end + + it 'returns the number of reset products or variants' do + reset_absent.call + expect(reset_absent.products_reset_count).to eq(2) + end + end end From ed073e9750ddf60ffae728b97a947889fca71090 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 24 Sep 2018 14:15:57 +0200 Subject: [PATCH 139/190] Rely on Settings and don't access internal struct. --- app/models/product_import/reset_absent.rb | 28 +++++++---- app/models/product_import/settings.rb | 16 +++++- spec/models/product_import/settings_spec.rb | 54 +++++++++++++++++++++ 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 439945bb06..54d3340c93 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -7,25 +7,29 @@ module ProductImport def initialize(decorated) super @products_reset_count = 0 + + settings = ProductImport::Settings.new(import_settings) + @settings = settings.settings + @updated_ids = settings.updated_ids + @enterprises_to_reset = settings.enterprises_to_reset end def call # For selected enterprises; set stock to zero for all products/inventory # that were not listed in the newly uploaded spreadsheet return unless data_for_stock_reset? + suppliers_to_reset_products = [] suppliers_to_reset_inventories = [] - settings = import_settings[:settings] - - import_settings[:enterprises_to_reset].each do |enterprise_id| - if settings['reset_all_absent'] && + enterprises_to_reset.each do |enterprise_id| + if reset_all_absent? && permission_by_id?(enterprise_id) && !importing_into_inventory? suppliers_to_reset_products.push(Integer(enterprise_id)) end - if settings['reset_all_absent'] && + if reset_all_absent? && permission_by_id?(enterprise_id) && importing_into_inventory? suppliers_to_reset_inventories.push(Integer(enterprise_id)) @@ -38,7 +42,7 @@ module ProductImport 'variant_overrides.hub_id IN (?) ' \ 'AND variant_overrides.id NOT IN (?)', suppliers_to_reset_inventories, - import_settings[:updated_ids] + updated_ids ) @products_reset_count += relation.update_all(count_on_hand: 0) end @@ -53,17 +57,21 @@ module ProductImport 'AND spree_variants.is_master = false ' \ 'AND spree_variants.deleted_at IS NULL', suppliers_to_reset_products, - import_settings[:updated_ids] + updated_ids ) @products_reset_count += relation.update_all(count_on_hand: 0) end private + attr_reader :settings, :updated_ids, :enterprises_to_reset + def data_for_stock_reset? - import_settings[:settings] && - import_settings[:updated_ids] && - import_settings[:enterprises_to_reset] + settings && updated_ids && enterprises_to_reset + end + + def reset_all_absent? + settings['reset_all_absent'] end end end diff --git a/app/models/product_import/settings.rb b/app/models/product_import/settings.rb index a97ee544ff..c0ef038ac1 100644 --- a/app/models/product_import/settings.rb +++ b/app/models/product_import/settings.rb @@ -6,8 +6,20 @@ module ProductImport def defaults(entry) @import_settings.key?(:settings) && - @import_settings[:settings][entry.supplier_id.to_s] && - @import_settings[:settings][entry.supplier_id.to_s]['defaults'] + settings[entry.supplier_id.to_s] && + settings[entry.supplier_id.to_s]['defaults'] + end + + def settings + @import_settings[:settings] + end + + def updated_ids + @import_settings[:updated_ids] + end + + def enterprises_to_reset + @import_settings[:enterprises_to_reset] end end end diff --git a/spec/models/product_import/settings_spec.rb b/spec/models/product_import/settings_spec.rb index 768bc315ad..7673efe65b 100644 --- a/spec/models/product_import/settings_spec.rb +++ b/spec/models/product_import/settings_spec.rb @@ -48,4 +48,58 @@ describe ProductImport::Settings do end end end + + describe '#settings' do + context 'when settings are specified' do + let(:import_settings) { { settings: { foo: 'bar' } } } + + it 'returns them' do + expect(settings.settings).to eq(foo: 'bar') + end + end + + context 'when settings are not specified' do + let(:import_settings) { {} } + + it 'returns nil' do + expect(settings.settings).to be_nil + end + end + end + + describe '#updated_ids' do + context 'when updated_ids are specified' do + let(:import_settings) { { updated_ids: [2] } } + + it 'returns them' do + expect(settings.updated_ids).to eq([2]) + end + end + + context 'when updated_ids are not specified' do + let(:import_settings) { {} } + + it 'returns nil' do + expect(settings.updated_ids).to be_nil + end + end + end + + describe '#enterprises_to_reset' do + context 'when enterprises_to_reset are specified' do + let(:import_settings) { { enterprises_to_reset: [2] } } + + it 'returns them' do + expect(settings.enterprises_to_reset).to eq([2]) + end + end + + context 'when enterprises_to_reset are not specified' do + let(:import_settings) { {} } + + it 'returns nil' do + expect(settings.enterprises_to_reset).to be_nil + end + end + end end From b5766a2dd96230b81036289eba39a17df681b7f0 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 24 Sep 2018 14:39:22 +0200 Subject: [PATCH 140/190] Extract common conditional clauses This also turns local vars into ivars so that the behaviour can be thoroughly tested. These ivars are meant to be removed once this class is refactored further. Now there's no other way to ensure its state. --- app/models/product_import/reset_absent.rb | 26 +++++----- .../product_import/reset_absent_spec.rb | 49 +++++++++++++++++++ 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 54d3340c93..74ba1d41d9 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -19,35 +19,33 @@ module ProductImport # that were not listed in the newly uploaded spreadsheet return unless data_for_stock_reset? - suppliers_to_reset_products = [] - suppliers_to_reset_inventories = [] + @suppliers_to_reset_products = [] + @suppliers_to_reset_inventories = [] enterprises_to_reset.each do |enterprise_id| - if reset_all_absent? && - permission_by_id?(enterprise_id) && - !importing_into_inventory? - suppliers_to_reset_products.push(Integer(enterprise_id)) + next unless reset_all_absent? && permission_by_id?(enterprise_id) + + if !importing_into_inventory? + @suppliers_to_reset_products.push(Integer(enterprise_id)) end - if reset_all_absent? && - permission_by_id?(enterprise_id) && - importing_into_inventory? - suppliers_to_reset_inventories.push(Integer(enterprise_id)) + if importing_into_inventory? + @suppliers_to_reset_inventories.push(Integer(enterprise_id)) end end - unless suppliers_to_reset_inventories.empty? + unless @suppliers_to_reset_inventories.empty? relation = VariantOverride .where( 'variant_overrides.hub_id IN (?) ' \ 'AND variant_overrides.id NOT IN (?)', - suppliers_to_reset_inventories, + @suppliers_to_reset_inventories, updated_ids ) @products_reset_count += relation.update_all(count_on_hand: 0) end - return if suppliers_to_reset_products.empty? + return if @suppliers_to_reset_products.empty? relation = Spree::Variant .joins(:product) @@ -56,7 +54,7 @@ module ProductImport 'AND spree_variants.id NOT IN (?) ' \ 'AND spree_variants.is_master = false ' \ 'AND spree_variants.deleted_at IS NULL', - suppliers_to_reset_products, + @suppliers_to_reset_products, updated_ids ) @products_reset_count += relation.update_all(count_on_hand: 0) diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index f2addc9dc3..c6fa531af9 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -99,6 +99,55 @@ describe ProductImport::ResetAbsent do end end end + + context 'when reset_all_absent is not set' do + let(:import_settings) do + { + settings: { 'reset_all_absent' => false }, + updated_ids: [0], + enterprises_to_reset: [1] + } + end + + it 'does not reset anything' do + reset_absent.call + + suppliers_to_reset_products = reset_absent + .instance_variable_get('@suppliers_to_reset_products') + suppliers_to_reset_inventories = reset_absent + .instance_variable_get('@suppliers_to_reset_inventories') + + expect(suppliers_to_reset_products).to eq([]) + expect(suppliers_to_reset_inventories).to eq([]) + end + end + + context 'the enterprise has no permission' do + let(:import_settings) do + { + settings: { 'reset_all_absent' => true }, + updated_ids: [0], + enterprises_to_reset: [1] + } + end + + before do + allow(entry_processor) + .to receive(:permission_by_id?).with(1) { false } + end + + it 'does not reset anything' do + reset_absent.call + + suppliers_to_reset_products = reset_absent + .instance_variable_get('@suppliers_to_reset_products') + suppliers_to_reset_inventories = reset_absent + .instance_variable_get('@suppliers_to_reset_inventories') + + expect(suppliers_to_reset_products).to eq([]) + expect(suppliers_to_reset_inventories).to eq([]) + end + end end describe '#products_reset_count' do From 5ac3598550e27f19e3a74013990f6cad3ca8d317 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 24 Sep 2018 17:01:48 +0200 Subject: [PATCH 141/190] Inject Settings object in ResetAbsent --- app/models/product_import/entry_processor.rb | 5 +- app/models/product_import/reset_absent.rb | 5 +- .../product_import/entry_processor_spec.rb | 12 ++-- .../product_import/reset_absent_spec.rb | 61 ++++++++++++++----- 4 files changed, 58 insertions(+), 25 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index a6ace4d659..d33d417608 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -65,7 +65,10 @@ module ProductImport end def reset_absent - @reset_absent ||= ResetAbsent.new(self) + @reset_absent ||= ResetAbsent.new( + self, + Settings.new(import_settings) + ) end def total_saved_count diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 74ba1d41d9..d0b11ffe02 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -4,11 +4,10 @@ module ProductImport class ResetAbsent < SimpleDelegator attr_reader :products_reset_count - def initialize(decorated) - super + def initialize(decorated, settings) + super(decorated) @products_reset_count = 0 - settings = ProductImport::Settings.new(import_settings) @settings = settings.settings @updated_ids = settings.updated_ids @enterprises_to_reset = settings.enterprises_to_reset diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index 88205a85d3..343a13f6d4 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -22,19 +22,21 @@ describe ProductImport::EntryProcessor do end describe '#reset_absent_items' do - let(:reset_absent) { instance_double(ProductImport::ResetAbsent, call: true) } + let(:reset_absent) do + instance_double(ProductImport::ResetAbsent, call: true) + end + let(:settings) { instance_double(ProductImport::Settings) } before do - allow(ProductImport::ResetAbsent) - .to receive(:new) - .and_return(reset_absent) + allow(ProductImport::ResetAbsent).to receive(:new) { reset_absent } + allow(ProductImport::Settings).to receive(:new) { settings } end it 'delegates to ResetAbsent' do entry_processor.reset_absent_items expect(ProductImport::ResetAbsent) - .to have_received(:new).with(entry_processor) + .to have_received(:new).with(entry_processor, settings) end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index c6fa531af9..046accf848 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -7,6 +7,7 @@ describe ProductImport::ResetAbsent do let(:editable_enterprises) { double(:editable_enterprises) } let(:import_time) { double(:import_time) } let(:updated_ids) { double(:updated_ids) } + let(:import_settings) { double(:import_settings) } let(:entry_processor) do ProductImport::EntryProcessor.new( @@ -20,11 +21,18 @@ describe ProductImport::ResetAbsent do ) end - let(:reset_absent) { described_class.new(entry_processor) } + let(:reset_absent) { described_class.new(entry_processor, settings) } describe '#call' do context 'when there are no settings' do - let(:import_settings) { { updated_ids: [], enterprises_to_reset: [] } } + let(:settings) do + instance_double( + ProductImport::Settings, + settings: nil, + updated_ids: [], + enterprises_to_reset: [] + ) + end it 'returns nil' do expect(reset_absent.call).to be_nil @@ -32,7 +40,14 @@ describe ProductImport::ResetAbsent do end context 'when there are no updated_ids' do - let(:import_settings) { { settings: [], enterprises_to_reset: [] } } + let(:settings) do + instance_double( + ProductImport::Settings, + settings: [], + updated_ids: nil, + enterprises_to_reset: [] + ) + end it 'returns nil' do expect(reset_absent.call).to be_nil @@ -40,7 +55,14 @@ describe ProductImport::ResetAbsent do end context 'when there are no enterprises_to_reset' do - let(:import_settings) { { settings: [], updated_ids: [] } } + let(:settings) do + instance_double( + ProductImport::Settings, + settings: [], + updated_ids: [], + enterprises_to_reset: nil + ) + end it 'returns nil' do expect(reset_absent.call).to be_nil @@ -48,12 +70,13 @@ describe ProductImport::ResetAbsent do end context 'when there are settings, updated_ids and enterprises_to_reset' do - let(:import_settings) do - { + let(:settings) do + instance_double( + ProductImport::Settings, settings: { 'reset_all_absent' => true }, updated_ids: [0], enterprises_to_reset: [enterprise.id] - } + ) end before do @@ -101,12 +124,13 @@ describe ProductImport::ResetAbsent do end context 'when reset_all_absent is not set' do - let(:import_settings) do - { + let(:settings) do + instance_double( + ProductImport::Settings, settings: { 'reset_all_absent' => false }, updated_ids: [0], enterprises_to_reset: [1] - } + ) end it 'does not reset anything' do @@ -123,12 +147,13 @@ describe ProductImport::ResetAbsent do end context 'the enterprise has no permission' do - let(:import_settings) do - { + let(:settings) do + instance_double( + ProductImport::Settings, settings: { 'reset_all_absent' => true }, updated_ids: [0], enterprises_to_reset: [1] - } + ) end before do @@ -157,14 +182,18 @@ describe ProductImport::ResetAbsent do before do allow(entry_processor) .to receive(:permission_by_id?).with(enterprise_id) { true } + + allow(entry_processor) + .to receive(:importing_into_inventory?) { false } end - let(:import_settings) do - { + let(:settings) do + instance_double( + ProductImport::Settings, settings: { 'reset_all_absent' => true }, updated_ids: [0], enterprises_to_reset: [enterprise_id] - } + ) end it 'returns the number of reset products or variants' do From e04162415a54801adf7929e5589dd863e7a08f1b Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 24 Sep 2018 17:04:15 +0200 Subject: [PATCH 142/190] Move code comment to be the method's doc --- app/models/product_import/reset_absent.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index d0b11ffe02..e4df27cedc 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -13,9 +13,9 @@ module ProductImport @enterprises_to_reset = settings.enterprises_to_reset end + # For selected enterprises; set stock to zero for all products/inventory + # that were not listed in the newly uploaded spreadsheet def call - # For selected enterprises; set stock to zero for all products/inventory - # that were not listed in the newly uploaded spreadsheet return unless data_for_stock_reset? @suppliers_to_reset_products = [] From c955e151b7c9e44fff0fa69d373201118c91376b Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 24 Sep 2018 17:53:51 +0200 Subject: [PATCH 143/190] Pass enterprise ids as strs as current code does --- app/models/product_import/reset_absent.rb | 4 ++-- spec/models/product_import/reset_absent_spec.rb | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index e4df27cedc..01e9b7f1d9 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -25,11 +25,11 @@ module ProductImport next unless reset_all_absent? && permission_by_id?(enterprise_id) if !importing_into_inventory? - @suppliers_to_reset_products.push(Integer(enterprise_id)) + @suppliers_to_reset_products << enterprise_id.to_i end if importing_into_inventory? - @suppliers_to_reset_inventories.push(Integer(enterprise_id)) + @suppliers_to_reset_inventories << enterprise_id.to_i end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 046accf848..b34e1ae4fa 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -75,13 +75,13 @@ describe ProductImport::ResetAbsent do ProductImport::Settings, settings: { 'reset_all_absent' => true }, updated_ids: [0], - enterprises_to_reset: [enterprise.id] + enterprises_to_reset: [enterprise.id.to_s] ) end before do allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise.id) { true } + .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } end context 'and not importing into inventory' do @@ -109,7 +109,7 @@ describe ProductImport::ResetAbsent do variant_override allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise.id) { true } + .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } end before do @@ -129,7 +129,7 @@ describe ProductImport::ResetAbsent do ProductImport::Settings, settings: { 'reset_all_absent' => false }, updated_ids: [0], - enterprises_to_reset: [1] + enterprises_to_reset: ['1'] ) end @@ -152,13 +152,13 @@ describe ProductImport::ResetAbsent do ProductImport::Settings, settings: { 'reset_all_absent' => true }, updated_ids: [0], - enterprises_to_reset: [1] + enterprises_to_reset: ['1'] ) end before do allow(entry_processor) - .to receive(:permission_by_id?).with(1) { false } + .to receive(:permission_by_id?).with('1') { false } end it 'does not reset anything' do @@ -181,7 +181,7 @@ describe ProductImport::ResetAbsent do before do allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise_id) { true } + .to receive(:permission_by_id?).with(enterprise_id.to_s) { true } allow(entry_processor) .to receive(:importing_into_inventory?) { false } @@ -192,7 +192,7 @@ describe ProductImport::ResetAbsent do ProductImport::Settings, settings: { 'reset_all_absent' => true }, updated_ids: [0], - enterprises_to_reset: [enterprise_id] + enterprises_to_reset: [enterprise_id.to_s] ) end From b940f06238972b0dfaea17598cd531d887ac3684 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 24 Sep 2018 18:06:20 +0200 Subject: [PATCH 144/190] Return early if reset_all_absent is not set No need to go through half of the algorithm if the setting is not enabled. It does not change per enterprise. This also assumes "Previously we were updating both (products and inventory) at the same time during one import, but now it's one or the other" in Matt's words. --- app/models/product_import/reset_absent.rb | 16 +++++++--------- spec/models/product_import/reset_absent_spec.rb | 7 ++++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 01e9b7f1d9..250369ba64 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -11,25 +11,23 @@ module ProductImport @settings = settings.settings @updated_ids = settings.updated_ids @enterprises_to_reset = settings.enterprises_to_reset + + @suppliers_to_reset_products = [] + @suppliers_to_reset_inventories = [] end # For selected enterprises; set stock to zero for all products/inventory # that were not listed in the newly uploaded spreadsheet def call - return unless data_for_stock_reset? - - @suppliers_to_reset_products = [] - @suppliers_to_reset_inventories = [] + return unless data_for_stock_reset? && reset_all_absent? enterprises_to_reset.each do |enterprise_id| - next unless reset_all_absent? && permission_by_id?(enterprise_id) - - if !importing_into_inventory? - @suppliers_to_reset_products << enterprise_id.to_i - end + next unless permission_by_id?(enterprise_id) if importing_into_inventory? @suppliers_to_reset_inventories << enterprise_id.to_i + else + @suppliers_to_reset_products << enterprise_id.to_i end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index b34e1ae4fa..47dc713d43 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -133,6 +133,11 @@ describe ProductImport::ResetAbsent do ) end + before do + allow(entry_processor) + .to receive(:permission_by_id?).with('1') { true } + end + it 'does not reset anything' do reset_absent.call @@ -146,7 +151,7 @@ describe ProductImport::ResetAbsent do end end - context 'the enterprise has no permission' do + context 'when the enterprise has no permission' do let(:settings) do instance_double( ProductImport::Settings, From fd84bea463ceda48fdd7cae62cc9d040e3546248 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 12:20:52 +0200 Subject: [PATCH 145/190] Test that variants or overrides are actually reset --- spec/models/product_import/reset_absent_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 47dc713d43..b6ac542ba2 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -96,6 +96,14 @@ describe ProductImport::ResetAbsent do it 'returns the number of products reset' do expect(reset_absent.call).to eq(2) end + + it 'resets the products of the specified suppliers' do + suppliers_to_reset_products = reset_absent + .instance_variable_get('@suppliers_to_reset_products') + + reset_absent.call + expect(suppliers_to_reset_products).to eq([enterprise.id]) + end end context 'and importing into inventory' do @@ -120,6 +128,14 @@ describe ProductImport::ResetAbsent do it 'returns nil' do expect(reset_absent.call).to be_nil end + + it 'resets the inventories of the specified suppliers' do + suppliers_to_reset_inventories = reset_absent + .instance_variable_get('@suppliers_to_reset_inventories') + + reset_absent.call + expect(suppliers_to_reset_inventories).to eq([enterprise.id]) + end end end From a409353d372f73c4040a6078c4e95c51760f7c3a Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 12:41:38 +0200 Subject: [PATCH 146/190] Move import_settings query methods to Settings --- app/models/product_import/entry_processor.rb | 11 +- app/models/product_import/reset_absent.rb | 22 +-- app/models/product_import/settings.rb | 12 ++ .../product_import/entry_processor_spec.rb | 15 +++ .../product_import/reset_absent_spec.rb | 42 ++---- spec/models/product_import/settings_spec.rb | 125 ++++++++++++++++++ 6 files changed, 175 insertions(+), 52 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index d33d417608..9afb1e3087 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -5,6 +5,7 @@ module ProductImport class EntryProcessor delegate :products_reset_count, to: :reset_absent + delegate :importing_into_inventory?, to: :settings attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :supplier_products, :total_supplier_products, :import_settings @@ -24,6 +25,8 @@ module ProductImport @variants_updated = 0 @supplier_products = {} @total_supplier_products = 0 + + @settings = Settings.new(import_settings) end def save_all(entries) @@ -79,12 +82,10 @@ module ProductImport @editable_enterprises.value?(Integer(supplier_id)) end - def importing_into_inventory? - import_settings[:settings] && import_settings[:settings]['import_into'] == 'inventories' - end - private + attr_reader :settings + def save_to_inventory(entry) save_new_inventory_item entry if entry.validates_as? 'new_inventory_item' save_existing_inventory_item entry if entry.validates_as? 'existing_inventory_item' @@ -183,8 +184,6 @@ module ProductImport end def assign_defaults(object, entry) - settings = Settings.new(import_settings) - # Assigns a default value for a specified field e.g. category='Vegetables', setting this value # either for all entries (overwrite_all), or only for those entries where the field was blank # in the spreadsheet (overwrite_empty), depending on selected import settings diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 250369ba64..022e456ead 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -8,9 +8,7 @@ module ProductImport super(decorated) @products_reset_count = 0 - @settings = settings.settings - @updated_ids = settings.updated_ids - @enterprises_to_reset = settings.enterprises_to_reset + @settings = settings @suppliers_to_reset_products = [] @suppliers_to_reset_inventories = [] @@ -19,9 +17,9 @@ module ProductImport # For selected enterprises; set stock to zero for all products/inventory # that were not listed in the newly uploaded spreadsheet def call - return unless data_for_stock_reset? && reset_all_absent? + return unless settings.data_for_stock_reset? && settings.reset_all_absent? - enterprises_to_reset.each do |enterprise_id| + settings.enterprises_to_reset.each do |enterprise_id| next unless permission_by_id?(enterprise_id) if importing_into_inventory? @@ -37,7 +35,7 @@ module ProductImport 'variant_overrides.hub_id IN (?) ' \ 'AND variant_overrides.id NOT IN (?)', @suppliers_to_reset_inventories, - updated_ids + settings.updated_ids ) @products_reset_count += relation.update_all(count_on_hand: 0) end @@ -52,21 +50,13 @@ module ProductImport 'AND spree_variants.is_master = false ' \ 'AND spree_variants.deleted_at IS NULL', @suppliers_to_reset_products, - updated_ids + settings.updated_ids ) @products_reset_count += relation.update_all(count_on_hand: 0) end private - attr_reader :settings, :updated_ids, :enterprises_to_reset - - def data_for_stock_reset? - settings && updated_ids && enterprises_to_reset - end - - def reset_all_absent? - settings['reset_all_absent'] - end + attr_reader :settings end end diff --git a/app/models/product_import/settings.rb b/app/models/product_import/settings.rb index c0ef038ac1..4a0df965cb 100644 --- a/app/models/product_import/settings.rb +++ b/app/models/product_import/settings.rb @@ -21,5 +21,17 @@ module ProductImport def enterprises_to_reset @import_settings[:enterprises_to_reset] end + + def importing_into_inventory? + settings && settings['import_into'] == 'inventories' + end + + def reset_all_absent? + settings['reset_all_absent'] + end + + def data_for_stock_reset? + !!(settings && updated_ids && enterprises_to_reset) + end end end diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index 343a13f6d4..152012cf58 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -56,4 +56,19 @@ describe ProductImport::EntryProcessor do expect(reset_absent).to have_received(:products_reset_count) end end + + describe '#importing_into_inventory?' do + let(:settings) do + instance_double(ProductImport::Settings, importing_into_inventory?: true) + end + + before do + allow(ProductImport::Settings).to receive(:new) { settings } + end + + it 'delegates to Settings' do + entry_processor.importing_into_inventory? + expect(settings).to have_received(:importing_into_inventory?) + end + end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index b6ac542ba2..4551c217d3 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -24,29 +24,9 @@ describe ProductImport::ResetAbsent do let(:reset_absent) { described_class.new(entry_processor, settings) } describe '#call' do - context 'when there are no settings' do + context 'when there is no data' do let(:settings) do - instance_double( - ProductImport::Settings, - settings: nil, - updated_ids: [], - enterprises_to_reset: [] - ) - end - - it 'returns nil' do - expect(reset_absent.call).to be_nil - end - end - - context 'when there are no updated_ids' do - let(:settings) do - instance_double( - ProductImport::Settings, - settings: [], - updated_ids: nil, - enterprises_to_reset: [] - ) + instance_double(ProductImport::Settings, data_for_stock_reset?: false) end it 'returns nil' do @@ -58,9 +38,7 @@ describe ProductImport::ResetAbsent do let(:settings) do instance_double( ProductImport::Settings, - settings: [], - updated_ids: [], - enterprises_to_reset: nil + enterprises_to_reset: [] ) end @@ -69,11 +47,12 @@ describe ProductImport::ResetAbsent do end end - context 'when there are settings, updated_ids and enterprises_to_reset' do + context 'when there are updated_ids and enterprises_to_reset' do let(:settings) do instance_double( ProductImport::Settings, - settings: { 'reset_all_absent' => true }, + reset_all_absent?: true, + data_for_stock_reset?: true, updated_ids: [0], enterprises_to_reset: [enterprise.id.to_s] ) @@ -143,7 +122,8 @@ describe ProductImport::ResetAbsent do let(:settings) do instance_double( ProductImport::Settings, - settings: { 'reset_all_absent' => false }, + reset_all_absent?: false, + data_for_stock_reset?: true, updated_ids: [0], enterprises_to_reset: ['1'] ) @@ -171,7 +151,8 @@ describe ProductImport::ResetAbsent do let(:settings) do instance_double( ProductImport::Settings, - settings: { 'reset_all_absent' => true }, + reset_all_absent?: true, + data_for_stock_reset?: true, updated_ids: [0], enterprises_to_reset: ['1'] ) @@ -211,7 +192,8 @@ describe ProductImport::ResetAbsent do let(:settings) do instance_double( ProductImport::Settings, - settings: { 'reset_all_absent' => true }, + reset_all_absent?: true, + data_for_stock_reset?: true, updated_ids: [0], enterprises_to_reset: [enterprise_id.to_s] ) diff --git a/spec/models/product_import/settings_spec.rb b/spec/models/product_import/settings_spec.rb index 7673efe65b..a7f823b2d8 100644 --- a/spec/models/product_import/settings_spec.rb +++ b/spec/models/product_import/settings_spec.rb @@ -102,4 +102,129 @@ describe ProductImport::Settings do end end end + + describe '#importing_into_inventory?' do + context 'when :settings is specified' do + context 'and import_into is not specified' do + let(:import_settings) { { settings: {} } } + + it 'returns false' do + expect(settings.importing_into_inventory?).to eq(false) + end + end + + context 'and import_into is equal to inventories' do + let(:import_settings) do + { settings: { 'import_into' => 'inventories' } } + end + + it 'returns true' do + expect(settings.importing_into_inventory?).to eq(true) + end + end + + context 'and import_into is not equal to inventories' do + let(:import_settings) do + { settings: { 'import_into' => 'other' } } + end + + it 'returns false' do + expect(settings.importing_into_inventory?).to eq(false) + end + end + end + + context 'when :settings is not specified' do + let(:import_settings) { {} } + + it 'returns falsy' do + expect(settings.importing_into_inventory?).to be_falsy + end + end + end + + describe '#reset_all_absent?' do + context 'when :settings is not specified' do + let(:import_settings) { {} } + + it 'raises' do + expect { settings.reset_all_absent? }.to raise_error(NoMethodError) + end + end + + context 'when reset_all_absent is not set' do + let(:import_settings) do + { settings: {} } + end + + it 'returns nil' do + expect(settings.reset_all_absent?).to be_nil + end + end + + context 'when reset_all_absent is set' do + let(:import_settings) do + { settings: { 'reset_all_absent' => true } } + end + + it 'returns true' do + expect(settings.reset_all_absent?).to eq(true) + end + end + end + + describe '#data_for_stock_reset?' do + context 'when there are no settings' do + let(:import_settings) do + { + updated_ids: [], + enterprises_to_reset: [] + } + end + + it 'returns false' do + expect(settings.data_for_stock_reset?).to eq(false) + end + end + + context 'when there are no updated_ids' do + let(:import_settings) do + { + settings: [], + enterprises_to_reset: [] + } + end + + it 'returns false' do + expect(settings.data_for_stock_reset?).to eq(false) + end + end + + context 'when there are no enterprises_to_reset' do + let(:import_settings) do + { + settings: [], + updated_ids: [] + } + end + + it 'returns false' do + expect(settings.data_for_stock_reset?).to eq(false) + end + end + + context 'when there are settings, updated_ids and enterprises_to_reset' do + let(:import_settings) do + { + settings: { 'something' => true }, + updated_ids: [0], + enterprises_to_reset: [0] + } + end + + it 'returns true' do + expect(settings.data_for_stock_reset?).to eq(true) + end + end + end end From 23471346b63cbb8e068a9c0de56e5d0356b75780 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 13:45:53 +0200 Subject: [PATCH 147/190] Do not call ResetAbsent when preconditions not met --- app/models/product_import/entry_processor.rb | 2 + app/models/product_import/reset_absent.rb | 2 - .../product_import/entry_processor_spec.rb | 49 +++++++++++++++++-- .../product_import/reset_absent_spec.rb | 39 --------------- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 9afb1e3087..5387da4be1 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -64,6 +64,8 @@ module ProductImport end def reset_absent_items + return unless settings.data_for_stock_reset? && settings.reset_all_absent? + reset_absent.call end diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 022e456ead..d1c0197d9c 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -17,8 +17,6 @@ module ProductImport # For selected enterprises; set stock to zero for all products/inventory # that were not listed in the newly uploaded spreadsheet def call - return unless settings.data_for_stock_reset? && settings.reset_all_absent? - settings.enterprises_to_reset.each do |enterprise_id| next unless permission_by_id?(enterprise_id) diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index 152012cf58..96c1b79b87 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -25,18 +25,57 @@ describe ProductImport::EntryProcessor do let(:reset_absent) do instance_double(ProductImport::ResetAbsent, call: true) end - let(:settings) { instance_double(ProductImport::Settings) } before do allow(ProductImport::ResetAbsent).to receive(:new) { reset_absent } allow(ProductImport::Settings).to receive(:new) { settings } end - it 'delegates to ResetAbsent' do - entry_processor.reset_absent_items + context 'when there is no data' do + let(:settings) do + instance_double( + ProductImport::Settings, + data_for_stock_reset?: false, + reset_all_absent?: true + ) + end - expect(ProductImport::ResetAbsent) - .to have_received(:new).with(entry_processor, settings) + it 'does not call ResetAbsent' do + entry_processor.reset_absent_items + expect(reset_absent).not_to have_received(:call) + end + end + + context 'when reset_all_absent is not set' do + let(:settings) do + instance_double( + ProductImport::Settings, + data_for_stock_reset?: true, + reset_all_absent?: false + ) + end + + it 'does not call ResetAbsent' do + entry_processor.reset_absent_items + expect(reset_absent).not_to have_received(:call) + end + end + + context 'when there is data and reset_all_absent is set' do + let(:settings) do + instance_double( + ProductImport::Settings, + data_for_stock_reset?: true, + reset_all_absent?: true + ) + end + + it 'delegates to ResetAbsent' do + entry_processor.reset_absent_items + + expect(ProductImport::ResetAbsent) + .to have_received(:new).with(entry_processor, settings) + end end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 4551c217d3..1d50e65a7d 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -24,16 +24,6 @@ describe ProductImport::ResetAbsent do let(:reset_absent) { described_class.new(entry_processor, settings) } describe '#call' do - context 'when there is no data' do - let(:settings) do - instance_double(ProductImport::Settings, data_for_stock_reset?: false) - end - - it 'returns nil' do - expect(reset_absent.call).to be_nil - end - end - context 'when there are no enterprises_to_reset' do let(:settings) do instance_double( @@ -118,35 +108,6 @@ describe ProductImport::ResetAbsent do end end - context 'when reset_all_absent is not set' do - let(:settings) do - instance_double( - ProductImport::Settings, - reset_all_absent?: false, - data_for_stock_reset?: true, - updated_ids: [0], - enterprises_to_reset: ['1'] - ) - end - - before do - allow(entry_processor) - .to receive(:permission_by_id?).with('1') { true } - end - - it 'does not reset anything' do - reset_absent.call - - suppliers_to_reset_products = reset_absent - .instance_variable_get('@suppliers_to_reset_products') - suppliers_to_reset_inventories = reset_absent - .instance_variable_get('@suppliers_to_reset_inventories') - - expect(suppliers_to_reset_products).to eq([]) - expect(suppliers_to_reset_inventories).to eq([]) - end - end - context 'when the enterprise has no permission' do let(:settings) do instance_double( From 8d7a11b0ac18067eaec2ad757fac6a6352069798 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 13:58:41 +0200 Subject: [PATCH 148/190] Make all steps of the algorithm have 2 branches --- app/models/product_import/reset_absent.rb | 29 +++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index d1c0197d9c..f3d08456d4 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -27,7 +27,7 @@ module ProductImport end end - unless @suppliers_to_reset_inventories.empty? + if @suppliers_to_reset_inventories.present? relation = VariantOverride .where( 'variant_overrides.hub_id IN (?) ' \ @@ -36,21 +36,20 @@ module ProductImport settings.updated_ids ) @products_reset_count += relation.update_all(count_on_hand: 0) + nil + elsif @suppliers_to_reset_products.present? + relation = Spree::Variant + .joins(:product) + .where( + 'spree_products.supplier_id IN (?) ' \ + 'AND spree_variants.id NOT IN (?) ' \ + 'AND spree_variants.is_master = false ' \ + 'AND spree_variants.deleted_at IS NULL', + @suppliers_to_reset_products, + settings.updated_ids + ) + @products_reset_count += relation.update_all(count_on_hand: 0) end - - return if @suppliers_to_reset_products.empty? - - relation = Spree::Variant - .joins(:product) - .where( - 'spree_products.supplier_id IN (?) ' \ - 'AND spree_variants.id NOT IN (?) ' \ - 'AND spree_variants.is_master = false ' \ - 'AND spree_variants.deleted_at IS NULL', - @suppliers_to_reset_products, - settings.updated_ids - ) - @products_reset_count += relation.update_all(count_on_hand: 0) end private From a9444b8909b7665545c689876989bf60825ced8d Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 16:47:04 +0200 Subject: [PATCH 149/190] Extract InventoryReset and Products strategies Extract this strategy classes from ResetAbsent and move #reset there. --- app/models/product_import/inventory_reset.rb | 25 +++++++ app/models/product_import/products_reset.rb | 29 +++++++ app/models/product_import/reset_absent.rb | 53 +++++++------ .../product_import/reset_absent_spec.rb | 75 +++++++++++++------ 4 files changed, 138 insertions(+), 44 deletions(-) create mode 100644 app/models/product_import/inventory_reset.rb create mode 100644 app/models/product_import/products_reset.rb diff --git a/app/models/product_import/inventory_reset.rb b/app/models/product_import/inventory_reset.rb new file mode 100644 index 0000000000..136a5856ee --- /dev/null +++ b/app/models/product_import/inventory_reset.rb @@ -0,0 +1,25 @@ +module ProductImport + class InventoryReset + def initialize(updated_ids, supplier_ids) + @updated_ids = updated_ids + @supplier_ids = supplier_ids + end + + def reset + relation.update_all(count_on_hand: 0) + end + + private + + attr_reader :updated_ids, :supplier_ids + + def relation + VariantOverride.where( + 'variant_overrides.hub_id IN (?) ' \ + 'AND variant_overrides.id NOT IN (?)', + supplier_ids, + updated_ids + ) + end + end +end diff --git a/app/models/product_import/products_reset.rb b/app/models/product_import/products_reset.rb new file mode 100644 index 0000000000..fd93c3b684 --- /dev/null +++ b/app/models/product_import/products_reset.rb @@ -0,0 +1,29 @@ +module ProductImport + class ProductsReset + def initialize(updated_ids, supplier_ids) + @updated_ids = updated_ids + @supplier_ids = supplier_ids + end + + def reset + relation.update_all(count_on_hand: 0) + end + + private + + attr_reader :updated_ids, :supplier_ids + + def relation + Spree::Variant + .joins(:product) + .where( + 'spree_products.supplier_id IN (?) ' \ + 'AND spree_variants.id NOT IN (?) ' \ + 'AND spree_variants.is_master = false ' \ + 'AND spree_variants.deleted_at IS NULL', + supplier_ids, + updated_ids + ) + end + end +end diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index f3d08456d4..8925c2386c 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -27,33 +27,40 @@ module ProductImport end end - if @suppliers_to_reset_inventories.present? - relation = VariantOverride - .where( - 'variant_overrides.hub_id IN (?) ' \ - 'AND variant_overrides.id NOT IN (?)', - @suppliers_to_reset_inventories, - settings.updated_ids - ) - @products_reset_count += relation.update_all(count_on_hand: 0) - nil - elsif @suppliers_to_reset_products.present? - relation = Spree::Variant - .joins(:product) - .where( - 'spree_products.supplier_id IN (?) ' \ - 'AND spree_variants.id NOT IN (?) ' \ - 'AND spree_variants.is_master = false ' \ - 'AND spree_variants.deleted_at IS NULL', - @suppliers_to_reset_products, - settings.updated_ids - ) - @products_reset_count += relation.update_all(count_on_hand: 0) - end + reset_stock if suppliers_to_reset? end private attr_reader :settings + + def reset_stock + @products_reset_count += strategy.reset + end + + def strategy + strategy_factory.new(settings.updated_ids, supplier_ids) + end + + def strategy_factory + if @suppliers_to_reset_inventories.present? + ProductImport::InventoryReset + elsif @suppliers_to_reset_products.present? + ProductImport::ProductsReset + end + end + + def supplier_ids + if @suppliers_to_reset_inventories.present? + @suppliers_to_reset_inventories + elsif @suppliers_to_reset_products.present? + @suppliers_to_reset_products + end + end + + def suppliers_to_reset? + @suppliers_to_reset_inventories.present? || + @suppliers_to_reset_products.present? + end end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 1d50e65a7d..6ebe7db206 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -94,8 +94,8 @@ describe ProductImport::ResetAbsent do .to receive(:importing_into_inventory?) { true } end - it 'returns nil' do - expect(reset_absent.call).to be_nil + it 'returns the number of products reset' do + expect(reset_absent.call).to eq(1) end it 'resets the inventories of the specified suppliers' do @@ -139,30 +139,63 @@ describe ProductImport::ResetAbsent do end describe '#products_reset_count' do - let(:variant) { create(:variant) } - let(:enterprise_id) { variant.product.supplier_id } + context 'and importing into inventory' do + let(:variant) { create(:variant) } + let(:enterprise) { variant.product.supplier } + let(:variant_override) do + create(:variant_override, variant: variant, hub: enterprise) + end - before do - allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise_id.to_s) { true } + let(:settings) do + instance_double( + ProductImport::Settings, + updated_ids: [0], + enterprises_to_reset: [enterprise.id.to_s] + ) + end - allow(entry_processor) - .to receive(:importing_into_inventory?) { false } + before do + variant_override + + allow(entry_processor) + .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } + end + + before do + allow(entry_processor) + .to receive(:importing_into_inventory?) { true } + end + + it 'returns the number of reset variant overrides' do + reset_absent.call + expect(reset_absent.products_reset_count).to eq(1) + end end - let(:settings) do - instance_double( - ProductImport::Settings, - reset_all_absent?: true, - data_for_stock_reset?: true, - updated_ids: [0], - enterprises_to_reset: [enterprise_id.to_s] - ) - end + context 'and not importing into inventory' do + let(:variant) { create(:variant) } + let(:enterprise_id) { variant.product.supplier_id } - it 'returns the number of reset products or variants' do - reset_absent.call - expect(reset_absent.products_reset_count).to eq(2) + before do + allow(entry_processor) + .to receive(:permission_by_id?).with(enterprise_id.to_s) { true } + + allow(entry_processor) + .to receive(:importing_into_inventory?) { false } + end + + let(:settings) do + instance_double( + ProductImport::Settings, + updated_ids: [0], + enterprises_to_reset: [enterprise_id.to_s] + ) + end + + it 'returns the number of reset products or variants' do + reset_absent.call + expect(reset_absent.products_reset_count).to eq(2) + end end end end From a10e58e20acb59a0e3ac5ebdef464ab12be4bd7d Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 17:53:21 +0200 Subject: [PATCH 150/190] Inject strategy_factory into ResetAbsent --- app/models/product_import/entry_processor.rb | 11 ++++++++++- app/models/product_import/reset_absent.rb | 13 +++---------- .../product_import/entry_processor_spec.rb | 8 ++++++-- spec/models/product_import/reset_absent_spec.rb | 16 +++++++++++++++- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 5387da4be1..085df93ad1 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -72,10 +72,19 @@ module ProductImport def reset_absent @reset_absent ||= ResetAbsent.new( self, - Settings.new(import_settings) + Settings.new(import_settings), + strategy_factory ) end + def strategy_factory + if importing_into_inventory? + InventoryReset + else + ProductsReset + end + end + def total_saved_count @products_created + @variants_created + @variants_updated + @inventory_created + @inventory_updated end diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 8925c2386c..94bdc6d997 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -4,11 +4,12 @@ module ProductImport class ResetAbsent < SimpleDelegator attr_reader :products_reset_count - def initialize(decorated, settings) + def initialize(decorated, settings, strategy_factory) super(decorated) @products_reset_count = 0 @settings = settings + @strategy_factory = strategy_factory @suppliers_to_reset_products = [] @suppliers_to_reset_inventories = [] @@ -32,7 +33,7 @@ module ProductImport private - attr_reader :settings + attr_reader :settings, :strategy_factory def reset_stock @products_reset_count += strategy.reset @@ -42,14 +43,6 @@ module ProductImport strategy_factory.new(settings.updated_ids, supplier_ids) end - def strategy_factory - if @suppliers_to_reset_inventories.present? - ProductImport::InventoryReset - elsif @suppliers_to_reset_products.present? - ProductImport::ProductsReset - end - end - def supplier_ids if @suppliers_to_reset_inventories.present? @suppliers_to_reset_inventories diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index 96c1b79b87..af6dbd4b17 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -66,7 +66,8 @@ describe ProductImport::EntryProcessor do instance_double( ProductImport::Settings, data_for_stock_reset?: true, - reset_all_absent?: true + reset_all_absent?: true, + importing_into_inventory?: true ) end @@ -74,7 +75,8 @@ describe ProductImport::EntryProcessor do entry_processor.reset_absent_items expect(ProductImport::ResetAbsent) - .to have_received(:new).with(entry_processor, settings) + .to have_received(:new) + .with(entry_processor, settings, ProductImport::InventoryReset) end end end @@ -88,6 +90,8 @@ describe ProductImport::EntryProcessor do .and_return(reset_absent) allow(reset_absent).to receive(:products_reset_count) + + allow(import_settings).to receive(:[]).with(:settings) end it 'delegates to ResetAbsent' do diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 6ebe7db206..dd965a9907 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -21,7 +21,9 @@ describe ProductImport::ResetAbsent do ) end - let(:reset_absent) { described_class.new(entry_processor, settings) } + let(:reset_absent) do + described_class.new(entry_processor, settings, strategy_factory) + end describe '#call' do context 'when there are no enterprises_to_reset' do @@ -32,6 +34,8 @@ describe ProductImport::ResetAbsent do ) end + let(:strategy_factory) { double(:strategy_factory) } + it 'returns nil' do expect(reset_absent.call).to be_nil end @@ -57,6 +61,8 @@ describe ProductImport::ResetAbsent do let(:variant) { create(:variant) } let(:enterprise) { variant.product.supplier } + let(:strategy_factory) { ProductImport::ProductsReset } + before do allow(entry_processor) .to receive(:importing_into_inventory?) { false } @@ -82,6 +88,8 @@ describe ProductImport::ResetAbsent do create(:variant_override, variant: variant, hub: enterprise) end + let(:strategy_factory) { ProductImport::InventoryReset } + before do variant_override @@ -119,6 +127,8 @@ describe ProductImport::ResetAbsent do ) end + let(:strategy_factory) { double(:strategy_factory) } + before do allow(entry_processor) .to receive(:permission_by_id?).with('1') { false } @@ -154,6 +164,8 @@ describe ProductImport::ResetAbsent do ) end + let(:strategy_factory) { ProductImport::InventoryReset } + before do variant_override @@ -192,6 +204,8 @@ describe ProductImport::ResetAbsent do ) end + let(:strategy_factory) { ProductImport::ProductsReset } + it 'returns the number of reset products or variants' do reset_absent.call expect(reset_absent.products_reset_count).to eq(2) From 54d6bc5443a708204449affb6655ddeb6d6cbd53 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 18:31:25 +0200 Subject: [PATCH 151/190] Don't use importing_into_inventory? in ResetAbsent This completely decouples ResetAbsent from the particular strategy used. --- app/models/product_import/entry_processor.rb | 4 +++ app/models/product_import/inventory_reset.rb | 16 ++++++--- app/models/product_import/products_reset.rb | 16 ++++++--- app/models/product_import/reset_absent.rb | 33 ++++--------------- .../product_import/reset_absent_spec.rb | 32 +++++++----------- 5 files changed, 44 insertions(+), 57 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 085df93ad1..5bdb0f6b4d 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -85,6 +85,10 @@ module ProductImport end end + def strategy + @strategy ||= strategy_factory.new + end + def total_saved_count @products_created + @variants_created + @variants_updated + @inventory_created + @inventory_updated end diff --git a/app/models/product_import/inventory_reset.rb b/app/models/product_import/inventory_reset.rb index 136a5856ee..7bc9b45dd5 100644 --- a/app/models/product_import/inventory_reset.rb +++ b/app/models/product_import/inventory_reset.rb @@ -1,17 +1,23 @@ module ProductImport class InventoryReset - def initialize(updated_ids, supplier_ids) - @updated_ids = updated_ids - @supplier_ids = supplier_ids + attr_reader :supplier_ids + + def initialize + @supplier_ids = [] end - def reset + def <<(values) + @supplier_ids << values + end + + def reset(updated_ids, _supplier_ids) + @updated_ids = updated_ids relation.update_all(count_on_hand: 0) end private - attr_reader :updated_ids, :supplier_ids + attr_reader :updated_ids def relation VariantOverride.where( diff --git a/app/models/product_import/products_reset.rb b/app/models/product_import/products_reset.rb index fd93c3b684..d3580bf2a7 100644 --- a/app/models/product_import/products_reset.rb +++ b/app/models/product_import/products_reset.rb @@ -1,17 +1,23 @@ module ProductImport class ProductsReset - def initialize(updated_ids, supplier_ids) - @updated_ids = updated_ids - @supplier_ids = supplier_ids + attr_reader :supplier_ids + + def initialize + @supplier_ids = [] end - def reset + def <<(values) + @supplier_ids << values + end + + def reset(updated_ids, _supplier_ids) + @updated_ids = updated_ids relation.update_all(count_on_hand: 0) end private - attr_reader :updated_ids, :supplier_ids + attr_reader :updated_ids def relation Spree::Variant diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 94bdc6d997..0c7526f7b0 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -10,9 +10,6 @@ module ProductImport @settings = settings @strategy_factory = strategy_factory - - @suppliers_to_reset_products = [] - @suppliers_to_reset_inventories = [] end # For selected enterprises; set stock to zero for all products/inventory @@ -21,14 +18,10 @@ module ProductImport settings.enterprises_to_reset.each do |enterprise_id| next unless permission_by_id?(enterprise_id) - if importing_into_inventory? - @suppliers_to_reset_inventories << enterprise_id.to_i - else - @suppliers_to_reset_products << enterprise_id.to_i - end + strategy << enterprise_id.to_i end - reset_stock if suppliers_to_reset? + reset_stock if strategy.supplier_ids end private @@ -36,24 +29,10 @@ module ProductImport attr_reader :settings, :strategy_factory def reset_stock - @products_reset_count += strategy.reset - end - - def strategy - strategy_factory.new(settings.updated_ids, supplier_ids) - end - - def supplier_ids - if @suppliers_to_reset_inventories.present? - @suppliers_to_reset_inventories - elsif @suppliers_to_reset_products.present? - @suppliers_to_reset_products - end - end - - def suppliers_to_reset? - @suppliers_to_reset_inventories.present? || - @suppliers_to_reset_products.present? + @products_reset_count += strategy.reset( + settings.updated_ids, + strategy.supplier_ids + ) end end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index dd965a9907..cd85a7dc4a 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -30,12 +30,17 @@ describe ProductImport::ResetAbsent do let(:settings) do instance_double( ProductImport::Settings, - enterprises_to_reset: [] + enterprises_to_reset: [], + updated_ids: [] ) end let(:strategy_factory) { double(:strategy_factory) } + before do + allow(import_settings).to receive(:[]).with(:settings) + end + it 'returns nil' do expect(reset_absent.call).to be_nil end @@ -72,12 +77,9 @@ describe ProductImport::ResetAbsent do expect(reset_absent.call).to eq(2) end - it 'resets the products of the specified suppliers' do - suppliers_to_reset_products = reset_absent - .instance_variable_get('@suppliers_to_reset_products') - + xit 'resets the products of the specified suppliers' do reset_absent.call - expect(suppliers_to_reset_products).to eq([enterprise.id]) + expect(strategy.supplier_ids).to eq([enterprise.id]) end end @@ -106,12 +108,9 @@ describe ProductImport::ResetAbsent do expect(reset_absent.call).to eq(1) end - it 'resets the inventories of the specified suppliers' do - suppliers_to_reset_inventories = reset_absent - .instance_variable_get('@suppliers_to_reset_inventories') - + xit 'resets the inventories of the specified suppliers' do reset_absent.call - expect(suppliers_to_reset_inventories).to eq([enterprise.id]) + expect(strategy.supplier_ids).to eq([enterprise.id]) end end end @@ -134,16 +133,9 @@ describe ProductImport::ResetAbsent do .to receive(:permission_by_id?).with('1') { false } end - it 'does not reset anything' do + xit 'does not reset anything' do reset_absent.call - - suppliers_to_reset_products = reset_absent - .instance_variable_get('@suppliers_to_reset_products') - suppliers_to_reset_inventories = reset_absent - .instance_variable_get('@suppliers_to_reset_inventories') - - expect(suppliers_to_reset_products).to eq([]) - expect(suppliers_to_reset_inventories).to eq([]) + expect(strategy.supplier_ids).to eq([]) end end end From 95ae18a1ba17f5966c968d000e75d23ab3cc9bf2 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 20:57:36 +0200 Subject: [PATCH 152/190] Remove method delegation --- app/models/product_import/entry_processor.rb | 5 ++--- .../models/product_import/entry_processor_spec.rb | 15 --------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 5bdb0f6b4d..e770e16931 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -5,7 +5,6 @@ module ProductImport class EntryProcessor delegate :products_reset_count, to: :reset_absent - delegate :importing_into_inventory?, to: :settings attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :supplier_products, :total_supplier_products, :import_settings @@ -47,7 +46,7 @@ module ProductImport next unless supplier_id && permission_by_id?(supplier_id) products_count = - if importing_into_inventory? + if settings.importing_into_inventory? VariantOverride.where('variant_overrides.hub_id IN (?)', supplier_id).count else Spree::Variant. @@ -78,7 +77,7 @@ module ProductImport end def strategy_factory - if importing_into_inventory? + if settings.importing_into_inventory? InventoryReset else ProductsReset diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index af6dbd4b17..b062b6e90a 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -99,19 +99,4 @@ describe ProductImport::EntryProcessor do expect(reset_absent).to have_received(:products_reset_count) end end - - describe '#importing_into_inventory?' do - let(:settings) do - instance_double(ProductImport::Settings, importing_into_inventory?: true) - end - - before do - allow(ProductImport::Settings).to receive(:new) { settings } - end - - it 'delegates to Settings' do - entry_processor.importing_into_inventory? - expect(settings).to have_received(:importing_into_inventory?) - end - end end From f04fa4ed63a32f207a7e657d4bc469262e236db3 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 21:03:01 +0200 Subject: [PATCH 153/190] Do not treat ResetAbsent as a decorator anymore This fully encapsulates it's logic and reduces its tight coupling with EntryProcessor. --- app/models/product_import/entry_processor.rb | 6 +- app/models/product_import/reset_absent.rb | 20 +- .../product_import/entry_processor_spec.rb | 54 +++++- .../product_import/reset_absent_spec.rb | 180 +++++------------- 4 files changed, 98 insertions(+), 162 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index e770e16931..05ca71c8d4 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -69,11 +69,7 @@ module ProductImport end def reset_absent - @reset_absent ||= ResetAbsent.new( - self, - Settings.new(import_settings), - strategy_factory - ) + @reset_absent ||= ResetAbsent.new(self, settings, strategy) end def strategy_factory diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 0c7526f7b0..593b74f1b2 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -1,32 +1,30 @@ -require 'delegate' - module ProductImport - class ResetAbsent < SimpleDelegator + class ResetAbsent attr_reader :products_reset_count - def initialize(decorated, settings, strategy_factory) - super(decorated) - @products_reset_count = 0 - + def initialize(entry_processor, settings, strategy) + @entry_processor = entry_processor @settings = settings - @strategy_factory = strategy_factory + @strategy = strategy + + @products_reset_count = 0 end # For selected enterprises; set stock to zero for all products/inventory # that were not listed in the newly uploaded spreadsheet def call settings.enterprises_to_reset.each do |enterprise_id| - next unless permission_by_id?(enterprise_id) + next unless entry_processor.permission_by_id?(enterprise_id) strategy << enterprise_id.to_i end - reset_stock if strategy.supplier_ids + reset_stock if strategy.supplier_ids.present? end private - attr_reader :settings, :strategy_factory + attr_reader :settings, :strategy, :entry_processor def reset_stock @products_reset_count += strategy.reset( diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index b062b6e90a..1f2e7eda62 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -67,31 +67,65 @@ describe ProductImport::EntryProcessor do ProductImport::Settings, data_for_stock_reset?: true, reset_all_absent?: true, - importing_into_inventory?: true + updated_ids: [1] ) end + context 'when importing into inventory' do + let(:strategy) { instance_double(ProductImport::InventoryReset) } - it 'delegates to ResetAbsent' do - entry_processor.reset_absent_items + before do + allow(settings).to receive(:importing_into_inventory?) { true } - expect(ProductImport::ResetAbsent) - .to have_received(:new) - .with(entry_processor, settings, ProductImport::InventoryReset) + allow(ProductImport::InventoryReset) + .to receive(:new).with([1]) { strategy } + end + + it 'delegates to ResetAbsent passing the appropriate strategy' do + entry_processor.reset_absent_items + + expect(ProductImport::ResetAbsent) + .to have_received(:new) + .with(entry_processor, settings, strategy) + end + end + + context 'when not importing into inventory' do + let(:strategy) { instance_double(ProductImport::ProductsReset) } + + before do + allow(settings).to receive(:importing_into_inventory?) { false } + + allow(ProductImport::ProductsReset) + .to receive(:new).with([1]) { strategy } + end + + it 'delegates to ResetAbsent passing the appropriate strategy' do + entry_processor.reset_absent_items + + expect(ProductImport::ResetAbsent) + .to have_received(:new) + .with(entry_processor, settings, strategy) + end end end end describe '#products_reset_count' do let(:reset_absent) { instance_double(ProductImport::ResetAbsent) } + let(:settings) do + instance_double( + ProductImport::Settings, + importing_into_inventory?: false, + updated_ids: [1] + ) + end before do + allow(ProductImport::Settings).to receive(:new) { settings } allow(ProductImport::ResetAbsent) - .to receive(:new) - .and_return(reset_absent) + .to receive(:new).and_return(reset_absent) allow(reset_absent).to receive(:products_reset_count) - - allow(import_settings).to receive(:[]).with(:settings) end it 'delegates to ResetAbsent' do diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index cd85a7dc4a..3e0ca117d5 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -1,28 +1,10 @@ require 'spec_helper' describe ProductImport::ResetAbsent do - let(:importer) { double(:importer) } - let(:validator) { double(:validator) } - let(:spreadsheet_data) { double(:spreadsheet_data) } - let(:editable_enterprises) { double(:editable_enterprises) } - let(:import_time) { double(:import_time) } - let(:updated_ids) { double(:updated_ids) } - let(:import_settings) { double(:import_settings) } - - let(:entry_processor) do - ProductImport::EntryProcessor.new( - importer, - validator, - import_settings, - spreadsheet_data, - editable_enterprises, - import_time, - updated_ids - ) - end + let(:entry_processor) { instance_double(ProductImport::EntryProcessor) } let(:reset_absent) do - described_class.new(entry_processor, settings, strategy_factory) + described_class.new(entry_processor, settings, strategy) end describe '#call' do @@ -35,10 +17,8 @@ describe ProductImport::ResetAbsent do ) end - let(:strategy_factory) { double(:strategy_factory) } - - before do - allow(import_settings).to receive(:[]).with(:settings) + let(:strategy) do + instance_double(ProductImport::InventoryReset, supplier_ids: []) end it 'returns nil' do @@ -47,161 +27,89 @@ describe ProductImport::ResetAbsent do end context 'when there are updated_ids and enterprises_to_reset' do + let(:enterprise) { instance_double(Enterprise, id: 1) } + let(:settings) do instance_double( ProductImport::Settings, - reset_all_absent?: true, - data_for_stock_reset?: true, updated_ids: [0], enterprises_to_reset: [enterprise.id.to_s] ) end + let(:strategy) { instance_double(ProductImport::ProductsReset) } + before do allow(entry_processor) .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } + + allow(strategy).to receive(:<<).with(enterprise.id) + allow(strategy).to receive(:supplier_ids) { [enterprise.id] } + allow(strategy).to receive(:reset).with([0], [enterprise.id]) { 2 } end - context 'and not importing into inventory' do - let(:variant) { create(:variant) } - let(:enterprise) { variant.product.supplier } - - let(:strategy_factory) { ProductImport::ProductsReset } - - before do - allow(entry_processor) - .to receive(:importing_into_inventory?) { false } - end - - it 'returns the number of products reset' do - expect(reset_absent.call).to eq(2) - end - - xit 'resets the products of the specified suppliers' do - reset_absent.call - expect(strategy.supplier_ids).to eq([enterprise.id]) - end + it 'returns the number of products reset' do + expect(reset_absent.call).to eq(2) end - context 'and importing into inventory' do - let(:variant) { create(:variant) } - let(:enterprise) { variant.product.supplier } - let(:variant_override) do - create(:variant_override, variant: variant, hub: enterprise) - end - - let(:strategy_factory) { ProductImport::InventoryReset } - - before do - variant_override - - allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } - end - - before do - allow(entry_processor) - .to receive(:importing_into_inventory?) { true } - end - - it 'returns the number of products reset' do - expect(reset_absent.call).to eq(1) - end - - xit 'resets the inventories of the specified suppliers' do - reset_absent.call - expect(strategy.supplier_ids).to eq([enterprise.id]) - end + it 'resets the products of the specified suppliers' do + expect(strategy).to receive(:reset).with([0], [enterprise.id]) { 2 } + reset_absent.call end end context 'when the enterprise has no permission' do + let(:enterprise) { instance_double(Enterprise, id: 1) } + let(:settings) do instance_double( ProductImport::Settings, - reset_all_absent?: true, - data_for_stock_reset?: true, updated_ids: [0], - enterprises_to_reset: ['1'] + enterprises_to_reset: [enterprise.id.to_s] ) end - let(:strategy_factory) { double(:strategy_factory) } + let(:strategy) { instance_double(ProductImport::InventoryReset) } before do allow(entry_processor) - .to receive(:permission_by_id?).with('1') { false } + .to receive(:permission_by_id?).with(enterprise.id.to_s) { false } + + allow(strategy).to receive(:supplier_ids) { [] } end - xit 'does not reset anything' do + it 'does not reset stock' do + expect(strategy).not_to receive(:reset) reset_absent.call - expect(strategy.supplier_ids).to eq([]) end end end describe '#products_reset_count' do - context 'and importing into inventory' do - let(:variant) { create(:variant) } - let(:enterprise) { variant.product.supplier } - let(:variant_override) do - create(:variant_override, variant: variant, hub: enterprise) - end + let(:enterprise) { instance_double(Enterprise, id: 1) } - let(:settings) do - instance_double( - ProductImport::Settings, - updated_ids: [0], - enterprises_to_reset: [enterprise.id.to_s] - ) - end - - let(:strategy_factory) { ProductImport::InventoryReset } - - before do - variant_override - - allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } - end - - before do - allow(entry_processor) - .to receive(:importing_into_inventory?) { true } - end - - it 'returns the number of reset variant overrides' do - reset_absent.call - expect(reset_absent.products_reset_count).to eq(1) - end + let(:settings) do + instance_double( + ProductImport::Settings, + updated_ids: [0], + enterprises_to_reset: [enterprise.id.to_s] + ) end - context 'and not importing into inventory' do - let(:variant) { create(:variant) } - let(:enterprise_id) { variant.product.supplier_id } + let(:strategy) { instance_double(ProductImport::InventoryReset) } - before do - allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise_id.to_s) { true } + before do + allow(entry_processor) + .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } - allow(entry_processor) - .to receive(:importing_into_inventory?) { false } - end + allow(strategy).to receive(:<<).with(enterprise.id) + allow(strategy).to receive(:supplier_ids) { [enterprise.id] } + allow(strategy).to receive(:reset).with([0], [enterprise.id]) { 1 } + end - let(:settings) do - instance_double( - ProductImport::Settings, - updated_ids: [0], - enterprises_to_reset: [enterprise_id.to_s] - ) - end - - let(:strategy_factory) { ProductImport::ProductsReset } - - it 'returns the number of reset products or variants' do - reset_absent.call - expect(reset_absent.products_reset_count).to eq(2) - end + it 'returns the number of reset variant overrides' do + reset_absent.call + expect(reset_absent.products_reset_count).to eq(1) end end end From 718f529ede96511a78932ca0a2ab5be3c0c356e9 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 21:31:34 +0200 Subject: [PATCH 154/190] Use nested module in test to improve readability --- .../product_import/reset_absent_spec.rb | 164 +++++++++--------- 1 file changed, 83 insertions(+), 81 deletions(-) diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 3e0ca117d5..76826a1fc2 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -1,43 +1,103 @@ require 'spec_helper' -describe ProductImport::ResetAbsent do - let(:entry_processor) { instance_double(ProductImport::EntryProcessor) } +module ProductImport + describe ResetAbsent do + let(:entry_processor) { instance_double(EntryProcessor) } - let(:reset_absent) do - described_class.new(entry_processor, settings, strategy) - end + let(:reset_absent) do + described_class.new(entry_processor, settings, strategy) + end - describe '#call' do - context 'when there are no enterprises_to_reset' do - let(:settings) do - instance_double( - ProductImport::Settings, - enterprises_to_reset: [], - updated_ids: [] - ) + describe '#call' do + context 'when there are no enterprises_to_reset' do + let(:settings) do + instance_double( + Settings, + enterprises_to_reset: [], + updated_ids: [] + ) + end + + let(:strategy) do + instance_double(InventoryReset, supplier_ids: []) + end + + it 'returns nil' do + expect(reset_absent.call).to be_nil + end end - let(:strategy) do - instance_double(ProductImport::InventoryReset, supplier_ids: []) + context 'when there are updated_ids and enterprises_to_reset' do + let(:enterprise) { instance_double(Enterprise, id: 1) } + + let(:settings) do + instance_double( + Settings, + updated_ids: [0], + enterprises_to_reset: [enterprise.id.to_s] + ) + end + + let(:strategy) { instance_double(ProductsReset) } + + before do + allow(entry_processor) + .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } + + allow(strategy).to receive(:<<).with(enterprise.id) + allow(strategy).to receive(:supplier_ids) { [enterprise.id] } + allow(strategy).to receive(:reset).with([0], [enterprise.id]) { 2 } + end + + it 'returns the number of products reset' do + expect(reset_absent.call).to eq(2) + end + + it 'resets the products of the specified suppliers' do + expect(strategy).to receive(:reset).with([0], [enterprise.id]) { 2 } + reset_absent.call + end end - it 'returns nil' do - expect(reset_absent.call).to be_nil + context 'when the enterprise has no permission' do + let(:enterprise) { instance_double(Enterprise, id: 1) } + + let(:settings) do + instance_double( + Settings, + updated_ids: [0], + enterprises_to_reset: [enterprise.id.to_s] + ) + end + + let(:strategy) { instance_double(InventoryReset) } + + before do + allow(entry_processor) + .to receive(:permission_by_id?).with(enterprise.id.to_s) { false } + + allow(strategy).to receive(:supplier_ids) { [] } + end + + it 'does not reset stock' do + expect(strategy).not_to receive(:reset) + reset_absent.call + end end end - context 'when there are updated_ids and enterprises_to_reset' do + describe '#products_reset_count' do let(:enterprise) { instance_double(Enterprise, id: 1) } let(:settings) do instance_double( - ProductImport::Settings, + Settings, updated_ids: [0], enterprises_to_reset: [enterprise.id.to_s] ) end - let(:strategy) { instance_double(ProductImport::ProductsReset) } + let(:strategy) { instance_double(InventoryReset) } before do allow(entry_processor) @@ -45,71 +105,13 @@ describe ProductImport::ResetAbsent do allow(strategy).to receive(:<<).with(enterprise.id) allow(strategy).to receive(:supplier_ids) { [enterprise.id] } - allow(strategy).to receive(:reset).with([0], [enterprise.id]) { 2 } + allow(strategy).to receive(:reset).with([0], [enterprise.id]) { 1 } end - it 'returns the number of products reset' do - expect(reset_absent.call).to eq(2) - end - - it 'resets the products of the specified suppliers' do - expect(strategy).to receive(:reset).with([0], [enterprise.id]) { 2 } + it 'returns the number of reset variant overrides' do reset_absent.call + expect(reset_absent.products_reset_count).to eq(1) end end - - context 'when the enterprise has no permission' do - let(:enterprise) { instance_double(Enterprise, id: 1) } - - let(:settings) do - instance_double( - ProductImport::Settings, - updated_ids: [0], - enterprises_to_reset: [enterprise.id.to_s] - ) - end - - let(:strategy) { instance_double(ProductImport::InventoryReset) } - - before do - allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise.id.to_s) { false } - - allow(strategy).to receive(:supplier_ids) { [] } - end - - it 'does not reset stock' do - expect(strategy).not_to receive(:reset) - reset_absent.call - end - end - end - - describe '#products_reset_count' do - let(:enterprise) { instance_double(Enterprise, id: 1) } - - let(:settings) do - instance_double( - ProductImport::Settings, - updated_ids: [0], - enterprises_to_reset: [enterprise.id.to_s] - ) - end - - let(:strategy) { instance_double(ProductImport::InventoryReset) } - - before do - allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } - - allow(strategy).to receive(:<<).with(enterprise.id) - allow(strategy).to receive(:supplier_ids) { [enterprise.id] } - allow(strategy).to receive(:reset).with([0], [enterprise.id]) { 1 } - end - - it 'returns the number of reset variant overrides' do - reset_absent.call - expect(reset_absent.products_reset_count).to eq(1) - end end end From 186801a1e2064b5e8ba55267116c2266927a97b4 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 21:38:11 +0200 Subject: [PATCH 155/190] Remove unused supplier_ids argument --- app/models/product_import/inventory_reset.rb | 2 +- app/models/product_import/products_reset.rb | 2 +- app/models/product_import/reset_absent.rb | 5 +---- spec/models/product_import/reset_absent_spec.rb | 6 +++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/models/product_import/inventory_reset.rb b/app/models/product_import/inventory_reset.rb index 7bc9b45dd5..cd064777c2 100644 --- a/app/models/product_import/inventory_reset.rb +++ b/app/models/product_import/inventory_reset.rb @@ -10,7 +10,7 @@ module ProductImport @supplier_ids << values end - def reset(updated_ids, _supplier_ids) + def reset(updated_ids) @updated_ids = updated_ids relation.update_all(count_on_hand: 0) end diff --git a/app/models/product_import/products_reset.rb b/app/models/product_import/products_reset.rb index d3580bf2a7..7462a56523 100644 --- a/app/models/product_import/products_reset.rb +++ b/app/models/product_import/products_reset.rb @@ -10,7 +10,7 @@ module ProductImport @supplier_ids << values end - def reset(updated_ids, _supplier_ids) + def reset(updated_ids) @updated_ids = updated_ids relation.update_all(count_on_hand: 0) end diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 593b74f1b2..06b2eb4c43 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -27,10 +27,7 @@ module ProductImport attr_reader :settings, :strategy, :entry_processor def reset_stock - @products_reset_count += strategy.reset( - settings.updated_ids, - strategy.supplier_ids - ) + @products_reset_count += strategy.reset(settings.updated_ids) end end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 76826a1fc2..5ef90d8f55 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -46,7 +46,7 @@ module ProductImport allow(strategy).to receive(:<<).with(enterprise.id) allow(strategy).to receive(:supplier_ids) { [enterprise.id] } - allow(strategy).to receive(:reset).with([0], [enterprise.id]) { 2 } + allow(strategy).to receive(:reset).with([0]) { 2 } end it 'returns the number of products reset' do @@ -54,7 +54,7 @@ module ProductImport end it 'resets the products of the specified suppliers' do - expect(strategy).to receive(:reset).with([0], [enterprise.id]) { 2 } + expect(strategy).to receive(:reset).with([0]) { 2 } reset_absent.call end end @@ -105,7 +105,7 @@ module ProductImport allow(strategy).to receive(:<<).with(enterprise.id) allow(strategy).to receive(:supplier_ids) { [enterprise.id] } - allow(strategy).to receive(:reset).with([0], [enterprise.id]) { 1 } + allow(strategy).to receive(:reset).with([0]) { 1 } end it 'returns the number of reset variant overrides' do From 5eb10edbfd4aeb2c971af4825f9a38fe7528a248 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 25 Sep 2018 21:43:08 +0200 Subject: [PATCH 156/190] Inject #updated_ids to strategy Their values are known beforehand. --- app/models/product_import/entry_processor.rb | 2 +- app/models/product_import/inventory_reset.rb | 6 +++--- app/models/product_import/products_reset.rb | 6 +++--- app/models/product_import/reset_absent.rb | 2 +- spec/models/product_import/reset_absent_spec.rb | 14 +++++--------- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 05ca71c8d4..8b45a2bf4f 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -81,7 +81,7 @@ module ProductImport end def strategy - @strategy ||= strategy_factory.new + @strategy ||= strategy_factory.new(settings.updated_ids) end def total_saved_count diff --git a/app/models/product_import/inventory_reset.rb b/app/models/product_import/inventory_reset.rb index cd064777c2..a03a008592 100644 --- a/app/models/product_import/inventory_reset.rb +++ b/app/models/product_import/inventory_reset.rb @@ -2,16 +2,16 @@ module ProductImport class InventoryReset attr_reader :supplier_ids - def initialize + def initialize(updated_ids) @supplier_ids = [] + @updated_ids = updated_ids end def <<(values) @supplier_ids << values end - def reset(updated_ids) - @updated_ids = updated_ids + def reset relation.update_all(count_on_hand: 0) end diff --git a/app/models/product_import/products_reset.rb b/app/models/product_import/products_reset.rb index 7462a56523..219b656f12 100644 --- a/app/models/product_import/products_reset.rb +++ b/app/models/product_import/products_reset.rb @@ -2,16 +2,16 @@ module ProductImport class ProductsReset attr_reader :supplier_ids - def initialize + def initialize(updated_ids) @supplier_ids = [] + @updated_ids = updated_ids end def <<(values) @supplier_ids << values end - def reset(updated_ids) - @updated_ids = updated_ids + def reset relation.update_all(count_on_hand: 0) end diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 06b2eb4c43..385b27d4ce 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -27,7 +27,7 @@ module ProductImport attr_reader :settings, :strategy, :entry_processor def reset_stock - @products_reset_count += strategy.reset(settings.updated_ids) + @products_reset_count += strategy.reset end end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 5ef90d8f55..e10a3a025b 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -13,8 +13,7 @@ module ProductImport let(:settings) do instance_double( Settings, - enterprises_to_reset: [], - updated_ids: [] + enterprises_to_reset: [] ) end @@ -27,13 +26,12 @@ module ProductImport end end - context 'when there are updated_ids and enterprises_to_reset' do + context 'when there are enterprises_to_reset' do let(:enterprise) { instance_double(Enterprise, id: 1) } let(:settings) do instance_double( Settings, - updated_ids: [0], enterprises_to_reset: [enterprise.id.to_s] ) end @@ -46,7 +44,7 @@ module ProductImport allow(strategy).to receive(:<<).with(enterprise.id) allow(strategy).to receive(:supplier_ids) { [enterprise.id] } - allow(strategy).to receive(:reset).with([0]) { 2 } + allow(strategy).to receive(:reset) { 2 } end it 'returns the number of products reset' do @@ -54,7 +52,7 @@ module ProductImport end it 'resets the products of the specified suppliers' do - expect(strategy).to receive(:reset).with([0]) { 2 } + expect(strategy).to receive(:reset) { 2 } reset_absent.call end end @@ -65,7 +63,6 @@ module ProductImport let(:settings) do instance_double( Settings, - updated_ids: [0], enterprises_to_reset: [enterprise.id.to_s] ) end @@ -92,7 +89,6 @@ module ProductImport let(:settings) do instance_double( Settings, - updated_ids: [0], enterprises_to_reset: [enterprise.id.to_s] ) end @@ -105,7 +101,7 @@ module ProductImport allow(strategy).to receive(:<<).with(enterprise.id) allow(strategy).to receive(:supplier_ids) { [enterprise.id] } - allow(strategy).to receive(:reset).with([0]) { 1 } + allow(strategy).to receive(:reset) { 1 } end it 'returns the number of reset variant overrides' do From d527f6265a883cb5ac3fbdc4e156c1a3cb674153 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 26 Sep 2018 11:37:55 +0200 Subject: [PATCH 157/190] Remove pointless sum --- app/models/product_import/reset_absent.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 385b27d4ce..854b19cc08 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -27,7 +27,7 @@ module ProductImport attr_reader :settings, :strategy, :entry_processor def reset_stock - @products_reset_count += strategy.reset + @products_reset_count = strategy.reset end end end From fd69c7672daf5e49554d039f0bc2e040bae69911 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 26 Sep 2018 11:38:22 +0200 Subject: [PATCH 158/190] Add specs for ResetAbsent strategies This also fixes the case where there are no overrides to exclude. --- app/models/product_import/inventory_reset.rb | 16 ++--- app/models/product_import/products_reset.rb | 20 +++--- .../product_import/inventory_reset_spec.rb | 70 +++++++++++++++++++ .../product_import/products_reset_spec.rb | 60 ++++++++++++++++ 4 files changed, 147 insertions(+), 19 deletions(-) create mode 100644 spec/models/product_import/inventory_reset_spec.rb create mode 100644 spec/models/product_import/products_reset_spec.rb diff --git a/app/models/product_import/inventory_reset.rb b/app/models/product_import/inventory_reset.rb index a03a008592..9aec385da4 100644 --- a/app/models/product_import/inventory_reset.rb +++ b/app/models/product_import/inventory_reset.rb @@ -2,9 +2,9 @@ module ProductImport class InventoryReset attr_reader :supplier_ids - def initialize(updated_ids) + def initialize(excluded_items_ids) @supplier_ids = [] - @updated_ids = updated_ids + @excluded_items_ids = excluded_items_ids end def <<(values) @@ -17,15 +17,13 @@ module ProductImport private - attr_reader :updated_ids + attr_reader :excluded_items_ids def relation - VariantOverride.where( - 'variant_overrides.hub_id IN (?) ' \ - 'AND variant_overrides.id NOT IN (?)', - supplier_ids, - updated_ids - ) + relation = VariantOverride.where(hub_id: supplier_ids) + return relation if excluded_items_ids.blank? + + relation.where('id NOT IN (?)', excluded_items_ids) end end end diff --git a/app/models/product_import/products_reset.rb b/app/models/product_import/products_reset.rb index 219b656f12..ad15283283 100644 --- a/app/models/product_import/products_reset.rb +++ b/app/models/product_import/products_reset.rb @@ -2,9 +2,9 @@ module ProductImport class ProductsReset attr_reader :supplier_ids - def initialize(updated_ids) + def initialize(excluded_items_ids) @supplier_ids = [] - @updated_ids = updated_ids + @excluded_items_ids = excluded_items_ids end def <<(values) @@ -17,19 +17,19 @@ module ProductImport private - attr_reader :updated_ids + attr_reader :excluded_items_ids def relation - Spree::Variant + relation = Spree::Variant .joins(:product) .where( - 'spree_products.supplier_id IN (?) ' \ - 'AND spree_variants.id NOT IN (?) ' \ - 'AND spree_variants.is_master = false ' \ - 'AND spree_variants.deleted_at IS NULL', - supplier_ids, - updated_ids + spree_products: { supplier_id: supplier_ids }, + spree_variants: { is_master: false, deleted_at: nil } ) + + return relation if excluded_items_ids.blank? + + relation.where('spree_variants.id NOT IN (?)', excluded_items_ids) end end end diff --git a/spec/models/product_import/inventory_reset_spec.rb b/spec/models/product_import/inventory_reset_spec.rb new file mode 100644 index 0000000000..412739ae10 --- /dev/null +++ b/spec/models/product_import/inventory_reset_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe ProductImport::InventoryReset do + let(:inventory_reset) { described_class.new(excluded_items_ids) } + + describe '#<<' do + let(:excluded_items_ids) { [] } + let(:supplier_ids) { 1 } + + it 'stores the specified supplier_ids' do + inventory_reset << supplier_ids + expect(inventory_reset.supplier_ids).to eq([supplier_ids]) + end + end + + describe '#reset' do + let(:supplier_ids) { enterprise.id } + let(:enterprise) { variant.product.supplier } + let(:variant) { create(:variant) } + + let!(:variant_override) do + create( + :variant_override, + count_on_hand: 10, + hub: enterprise, + variant: variant + ) + end + + before { inventory_reset << supplier_ids } + + context 'when there are excluded_items_ids' do + let(:excluded_items_ids) { [variant_override.id] } + + it 'does not update the count_on_hand of the excluded items' do + inventory_reset.reset + expect(variant_override.reload.count_on_hand).to eq(10) + end + + it 'updates the count_on_hand of the non-excluded items' do + non_excluded_variant_override = create( + :variant_override, + count_on_hand: 3, + hub: enterprise, + variant: variant + ) + inventory_reset.reset + expect(non_excluded_variant_override.reload.count_on_hand).to eq(0) + end + end + + context 'when there are no excluded_items_ids' do + let(:excluded_items_ids) { [] } + + it 'sets all count_on_hand to 0' do + inventory_reset.reset + expect(variant_override.reload.count_on_hand).to eq(0) + end + end + + context 'when excluded_items_ids is nil' do + let(:excluded_items_ids) { nil } + + it 'sets all count_on_hand to 0' do + inventory_reset.reset + expect(variant_override.reload.count_on_hand).to eq(0) + end + end + end +end diff --git a/spec/models/product_import/products_reset_spec.rb b/spec/models/product_import/products_reset_spec.rb new file mode 100644 index 0000000000..f84820ad81 --- /dev/null +++ b/spec/models/product_import/products_reset_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe ProductImport::ProductsReset do + let(:products_reset) { described_class.new(excluded_items_ids) } + + describe '#<<' do + let(:excluded_items_ids) { [] } + let(:supplier_ids) { 1 } + + it 'stores the specified supplier_ids' do + products_reset << supplier_ids + expect(products_reset.supplier_ids).to eq([supplier_ids]) + end + end + + describe '#reset' do + let(:supplier_ids) { enterprise.id } + let(:enterprise) { variant.product.supplier } + let(:variant) { create(:variant, count_on_hand: 2) } + + before { products_reset << supplier_ids } + + context 'when there are excluded_items_ids' do + let(:excluded_items_ids) { [variant.id] } + + it 'does not update the count_on_hand of the excluded items' do + products_reset.reset + expect(variant.reload.count_on_hand).to eq(2) + end + + it 'updates the count_on_hand of the non-excluded items' do + non_excluded_variant = create( + :variant, + count_on_hand: 3, + product: variant.product + ) + products_reset.reset + expect(non_excluded_variant.reload.count_on_hand).to eq(0) + end + end + + context 'when there are no excluded_items_ids' do + let(:excluded_items_ids) { [] } + + it 'sets all count_on_hand to 0' do + products_reset.reset + expect(variant.reload.count_on_hand).to eq(0) + end + end + + context 'when excluded_items_ids is nil' do + let(:excluded_items_ids) { nil } + + it 'sets all count_on_hand to 0' do + products_reset.reset + expect(variant.reload.count_on_hand).to eq(0) + end + end + end +end From 8715fce29583403d0983d080dc84d61e54ae8a58 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 27 Sep 2018 15:34:46 +0200 Subject: [PATCH 159/190] Remove @import_settings in favor of Settings --- app/models/product_import/entry_processor.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 8b45a2bf4f..38cd6a448f 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -6,12 +6,14 @@ module ProductImport class EntryProcessor delegate :products_reset_count, to: :reset_absent - attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :supplier_products, :total_supplier_products, :import_settings + attr_reader :inventory_created, :inventory_updated, :products_created, + :variants_created, :variants_updated, :supplier_products, + :total_supplier_products def initialize(importer, validator, import_settings, spreadsheet_data, editable_enterprises, import_time, updated_ids) @importer = importer @validator = validator - @import_settings = import_settings + @settings = Settings.new(import_settings) @spreadsheet_data = spreadsheet_data @editable_enterprises = editable_enterprises @import_time = import_time @@ -24,8 +26,6 @@ module ProductImport @variants_updated = 0 @supplier_products = {} @total_supplier_products = 0 - - @settings = Settings.new(import_settings) end def save_all(entries) @@ -122,7 +122,7 @@ module ProductImport end def import_into_inventory?(entry) - entry.supplier_id && import_settings[:settings]['import_into'] == 'inventories' + entry.supplier_id && settings.importing_into_inventory? end def save_new_inventory_item(entry) From af93af1a646ec05cb12814a3126820ec5b1a26a2 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 1 Oct 2018 15:53:26 +0200 Subject: [PATCH 160/190] Replace strategy with a more explicit name --- app/models/product_import/entry_processor.rb | 9 +++--- app/models/product_import/reset_absent.rb | 12 ++++---- .../product_import/entry_processor_spec.rb | 21 ++++++++----- .../product_import/reset_absent_spec.rb | 30 ++++++++++--------- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index 38cd6a448f..ae8e3de980 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -69,10 +69,10 @@ module ProductImport end def reset_absent - @reset_absent ||= ResetAbsent.new(self, settings, strategy) + @reset_absent ||= ResetAbsent.new(self, settings, reset_stock_strategy) end - def strategy_factory + def reset_stock_strategy_factory if settings.importing_into_inventory? InventoryReset else @@ -80,8 +80,9 @@ module ProductImport end end - def strategy - @strategy ||= strategy_factory.new(settings.updated_ids) + def reset_stock_strategy + @reset_stock_strategy ||= reset_stock_strategy_factory + .new(settings.updated_ids) end def total_saved_count diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 854b19cc08..69bdc37877 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -2,10 +2,10 @@ module ProductImport class ResetAbsent attr_reader :products_reset_count - def initialize(entry_processor, settings, strategy) + def initialize(entry_processor, settings, reset_stock_strategy) @entry_processor = entry_processor @settings = settings - @strategy = strategy + @reset_stock_strategy = reset_stock_strategy @products_reset_count = 0 end @@ -16,18 +16,18 @@ module ProductImport settings.enterprises_to_reset.each do |enterprise_id| next unless entry_processor.permission_by_id?(enterprise_id) - strategy << enterprise_id.to_i + reset_stock_strategy << enterprise_id.to_i end - reset_stock if strategy.supplier_ids.present? + reset_stock if reset_stock_strategy.supplier_ids.present? end private - attr_reader :settings, :strategy, :entry_processor + attr_reader :settings, :reset_stock_strategy, :entry_processor def reset_stock - @products_reset_count = strategy.reset + @products_reset_count = reset_stock_strategy.reset end end end diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index 1f2e7eda62..1fbf9198d4 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -70,41 +70,46 @@ describe ProductImport::EntryProcessor do updated_ids: [1] ) end + context 'when importing into inventory' do - let(:strategy) { instance_double(ProductImport::InventoryReset) } + let(:reset_stock_strategy) do + instance_double(ProductImport::InventoryReset) + end before do allow(settings).to receive(:importing_into_inventory?) { true } allow(ProductImport::InventoryReset) - .to receive(:new).with([1]) { strategy } + .to receive(:new).with([1]) { reset_stock_strategy } end - it 'delegates to ResetAbsent passing the appropriate strategy' do + it 'delegates to ResetAbsent passing the appropriate reset_stock_strategy' do entry_processor.reset_absent_items expect(ProductImport::ResetAbsent) .to have_received(:new) - .with(entry_processor, settings, strategy) + .with(entry_processor, settings, reset_stock_strategy) end end context 'when not importing into inventory' do - let(:strategy) { instance_double(ProductImport::ProductsReset) } + let(:reset_stock_strategy) do + instance_double(ProductImport::ProductsReset) + end before do allow(settings).to receive(:importing_into_inventory?) { false } allow(ProductImport::ProductsReset) - .to receive(:new).with([1]) { strategy } + .to receive(:new).with([1]) { reset_stock_strategy } end - it 'delegates to ResetAbsent passing the appropriate strategy' do + it 'delegates to ResetAbsent passing the appropriate reset_stock_strategy' do entry_processor.reset_absent_items expect(ProductImport::ResetAbsent) .to have_received(:new) - .with(entry_processor, settings, strategy) + .with(entry_processor, settings, reset_stock_strategy) end end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index e10a3a025b..4b04a4e052 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -5,7 +5,7 @@ module ProductImport let(:entry_processor) { instance_double(EntryProcessor) } let(:reset_absent) do - described_class.new(entry_processor, settings, strategy) + described_class.new(entry_processor, settings, reset_stock_strategy) end describe '#call' do @@ -17,7 +17,7 @@ module ProductImport ) end - let(:strategy) do + let(:reset_stock_strategy) do instance_double(InventoryReset, supplier_ids: []) end @@ -36,15 +36,16 @@ module ProductImport ) end - let(:strategy) { instance_double(ProductsReset) } + let(:reset_stock_strategy) { instance_double(ProductsReset) } before do allow(entry_processor) .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } - allow(strategy).to receive(:<<).with(enterprise.id) - allow(strategy).to receive(:supplier_ids) { [enterprise.id] } - allow(strategy).to receive(:reset) { 2 } + allow(reset_stock_strategy).to receive(:<<).with(enterprise.id) + allow(reset_stock_strategy) + .to receive(:supplier_ids) { [enterprise.id] } + allow(reset_stock_strategy).to receive(:reset) { 2 } end it 'returns the number of products reset' do @@ -52,7 +53,7 @@ module ProductImport end it 'resets the products of the specified suppliers' do - expect(strategy).to receive(:reset) { 2 } + expect(reset_stock_strategy).to receive(:reset) { 2 } reset_absent.call end end @@ -67,17 +68,17 @@ module ProductImport ) end - let(:strategy) { instance_double(InventoryReset) } + let(:reset_stock_strategy) { instance_double(InventoryReset) } before do allow(entry_processor) .to receive(:permission_by_id?).with(enterprise.id.to_s) { false } - allow(strategy).to receive(:supplier_ids) { [] } + allow(reset_stock_strategy).to receive(:supplier_ids) { [] } end it 'does not reset stock' do - expect(strategy).not_to receive(:reset) + expect(reset_stock_strategy).not_to receive(:reset) reset_absent.call end end @@ -93,15 +94,16 @@ module ProductImport ) end - let(:strategy) { instance_double(InventoryReset) } + let(:reset_stock_strategy) { instance_double(InventoryReset) } before do allow(entry_processor) .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } - allow(strategy).to receive(:<<).with(enterprise.id) - allow(strategy).to receive(:supplier_ids) { [enterprise.id] } - allow(strategy).to receive(:reset) { 1 } + allow(reset_stock_strategy).to receive(:<<).with(enterprise.id) + allow(reset_stock_strategy) + .to receive(:supplier_ids) { [enterprise.id] } + allow(reset_stock_strategy).to receive(:reset) { 1 } end it 'returns the number of reset variant overrides' do From 148321f7b73ccc62efce1f23d9653725b50e1ef2 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 1 Oct 2018 16:00:42 +0200 Subject: [PATCH 161/190] Make strategies class names more explicit --- app/models/product_import/entry_processor.rb | 4 ++-- .../{inventory_reset.rb => inventory_reset_strategy.rb} | 2 +- .../{products_reset.rb => products_reset_strategy.rb} | 2 +- spec/models/product_import/entry_processor_spec.rb | 8 ++++---- ...ory_reset_spec.rb => inventory_reset_strategy_spec.rb} | 2 +- ...ucts_reset_spec.rb => products_reset_strategy_spec.rb} | 2 +- spec/models/product_import/reset_absent_spec.rb | 8 ++++---- 7 files changed, 14 insertions(+), 14 deletions(-) rename app/models/product_import/{inventory_reset.rb => inventory_reset_strategy.rb} (94%) rename app/models/product_import/{products_reset.rb => products_reset_strategy.rb} (95%) rename spec/models/product_import/{inventory_reset_spec.rb => inventory_reset_strategy_spec.rb} (97%) rename spec/models/product_import/{products_reset_spec.rb => products_reset_strategy_spec.rb} (97%) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index ae8e3de980..e8ed57339b 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -74,9 +74,9 @@ module ProductImport def reset_stock_strategy_factory if settings.importing_into_inventory? - InventoryReset + InventoryResetStrategy else - ProductsReset + ProductsResetStrategy end end diff --git a/app/models/product_import/inventory_reset.rb b/app/models/product_import/inventory_reset_strategy.rb similarity index 94% rename from app/models/product_import/inventory_reset.rb rename to app/models/product_import/inventory_reset_strategy.rb index 9aec385da4..f730b22376 100644 --- a/app/models/product_import/inventory_reset.rb +++ b/app/models/product_import/inventory_reset_strategy.rb @@ -1,5 +1,5 @@ module ProductImport - class InventoryReset + class InventoryResetStrategy attr_reader :supplier_ids def initialize(excluded_items_ids) diff --git a/app/models/product_import/products_reset.rb b/app/models/product_import/products_reset_strategy.rb similarity index 95% rename from app/models/product_import/products_reset.rb rename to app/models/product_import/products_reset_strategy.rb index ad15283283..369ff07ac0 100644 --- a/app/models/product_import/products_reset.rb +++ b/app/models/product_import/products_reset_strategy.rb @@ -1,5 +1,5 @@ module ProductImport - class ProductsReset + class ProductsResetStrategy attr_reader :supplier_ids def initialize(excluded_items_ids) diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index 1fbf9198d4..48c8a8054d 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -73,13 +73,13 @@ describe ProductImport::EntryProcessor do context 'when importing into inventory' do let(:reset_stock_strategy) do - instance_double(ProductImport::InventoryReset) + instance_double(ProductImport::InventoryResetStrategy) end before do allow(settings).to receive(:importing_into_inventory?) { true } - allow(ProductImport::InventoryReset) + allow(ProductImport::InventoryResetStrategy) .to receive(:new).with([1]) { reset_stock_strategy } end @@ -94,13 +94,13 @@ describe ProductImport::EntryProcessor do context 'when not importing into inventory' do let(:reset_stock_strategy) do - instance_double(ProductImport::ProductsReset) + instance_double(ProductImport::ProductsResetStrategy) end before do allow(settings).to receive(:importing_into_inventory?) { false } - allow(ProductImport::ProductsReset) + allow(ProductImport::ProductsResetStrategy) .to receive(:new).with([1]) { reset_stock_strategy } end diff --git a/spec/models/product_import/inventory_reset_spec.rb b/spec/models/product_import/inventory_reset_strategy_spec.rb similarity index 97% rename from spec/models/product_import/inventory_reset_spec.rb rename to spec/models/product_import/inventory_reset_strategy_spec.rb index 412739ae10..7a86390871 100644 --- a/spec/models/product_import/inventory_reset_spec.rb +++ b/spec/models/product_import/inventory_reset_strategy_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ProductImport::InventoryReset do +describe ProductImport::InventoryResetStrategy do let(:inventory_reset) { described_class.new(excluded_items_ids) } describe '#<<' do diff --git a/spec/models/product_import/products_reset_spec.rb b/spec/models/product_import/products_reset_strategy_spec.rb similarity index 97% rename from spec/models/product_import/products_reset_spec.rb rename to spec/models/product_import/products_reset_strategy_spec.rb index f84820ad81..47c8fb393d 100644 --- a/spec/models/product_import/products_reset_spec.rb +++ b/spec/models/product_import/products_reset_strategy_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ProductImport::ProductsReset do +describe ProductImport::ProductsResetStrategy do let(:products_reset) { described_class.new(excluded_items_ids) } describe '#<<' do diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 4b04a4e052..14fab6de2c 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -18,7 +18,7 @@ module ProductImport end let(:reset_stock_strategy) do - instance_double(InventoryReset, supplier_ids: []) + instance_double(InventoryResetStrategy, supplier_ids: []) end it 'returns nil' do @@ -36,7 +36,7 @@ module ProductImport ) end - let(:reset_stock_strategy) { instance_double(ProductsReset) } + let(:reset_stock_strategy) { instance_double(ProductsResetStrategy) } before do allow(entry_processor) @@ -68,7 +68,7 @@ module ProductImport ) end - let(:reset_stock_strategy) { instance_double(InventoryReset) } + let(:reset_stock_strategy) { instance_double(InventoryResetStrategy) } before do allow(entry_processor) @@ -94,7 +94,7 @@ module ProductImport ) end - let(:reset_stock_strategy) { instance_double(InventoryReset) } + let(:reset_stock_strategy) { instance_double(InventoryResetStrategy) } before do allow(entry_processor) From 82654cd1ee5d151e6d0dcf5ca845b28bd38adde6 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 1 Oct 2018 16:33:46 +0200 Subject: [PATCH 162/190] Turn delegation to a reader to make it simpler This makes the solution less smart and as a result ResetAbsent improves it's consistency and returns always an integer. --- app/models/product_import/entry_processor.rb | 7 ++-- .../inventory_reset_strategy.rb | 6 +++- .../product_import/products_reset_strategy.rb | 6 +++- app/models/product_import/reset_absent.rb | 14 ++++---- .../product_import/entry_processor_spec.rb | 28 ++++++++++----- .../product_import/reset_absent_spec.rb | 34 +++---------------- 6 files changed, 45 insertions(+), 50 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index e8ed57339b..22bae53907 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -4,11 +4,9 @@ module ProductImport class EntryProcessor - delegate :products_reset_count, to: :reset_absent - attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :supplier_products, - :total_supplier_products + :total_supplier_products, :products_reset_count def initialize(importer, validator, import_settings, spreadsheet_data, editable_enterprises, import_time, updated_ids) @importer = importer @@ -24,6 +22,7 @@ module ProductImport @products_created = 0 @variants_created = 0 @variants_updated = 0 + @products_reset_count = 0 @supplier_products = {} @total_supplier_products = 0 end @@ -65,7 +64,7 @@ module ProductImport def reset_absent_items return unless settings.data_for_stock_reset? && settings.reset_all_absent? - reset_absent.call + @products_reset_count = reset_absent.call end def reset_absent diff --git a/app/models/product_import/inventory_reset_strategy.rb b/app/models/product_import/inventory_reset_strategy.rb index f730b22376..d89891e19f 100644 --- a/app/models/product_import/inventory_reset_strategy.rb +++ b/app/models/product_import/inventory_reset_strategy.rb @@ -12,7 +12,11 @@ module ProductImport end def reset - relation.update_all(count_on_hand: 0) + if supplier_ids.present? + relation.update_all(count_on_hand: 0) + else + 0 + end end private diff --git a/app/models/product_import/products_reset_strategy.rb b/app/models/product_import/products_reset_strategy.rb index 369ff07ac0..bdb6fd7d5e 100644 --- a/app/models/product_import/products_reset_strategy.rb +++ b/app/models/product_import/products_reset_strategy.rb @@ -12,7 +12,11 @@ module ProductImport end def reset - relation.update_all(count_on_hand: 0) + if supplier_ids.present? + relation.update_all(count_on_hand: 0) + else + 0 + end end private diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 69bdc37877..5e8d7d6bb3 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -1,17 +1,15 @@ module ProductImport class ResetAbsent - attr_reader :products_reset_count - def initialize(entry_processor, settings, reset_stock_strategy) @entry_processor = entry_processor @settings = settings @reset_stock_strategy = reset_stock_strategy - - @products_reset_count = 0 end # For selected enterprises; set stock to zero for all products/inventory # that were not listed in the newly uploaded spreadsheet + # + # @return [Integer] number of items affected by the reset def call settings.enterprises_to_reset.each do |enterprise_id| next unless entry_processor.permission_by_id?(enterprise_id) @@ -19,7 +17,7 @@ module ProductImport reset_stock_strategy << enterprise_id.to_i end - reset_stock if reset_stock_strategy.supplier_ids.present? + reset_stock end private @@ -27,7 +25,11 @@ module ProductImport attr_reader :settings, :reset_stock_strategy, :entry_processor def reset_stock - @products_reset_count = reset_stock_strategy.reset + if reset_stock_strategy.supplier_ids.present? + reset_stock_strategy.reset + else + 0 + end end end end diff --git a/spec/models/product_import/entry_processor_spec.rb b/spec/models/product_import/entry_processor_spec.rb index 48c8a8054d..ab854c5ec4 100644 --- a/spec/models/product_import/entry_processor_spec.rb +++ b/spec/models/product_import/entry_processor_spec.rb @@ -116,26 +116,36 @@ describe ProductImport::EntryProcessor do end describe '#products_reset_count' do - let(:reset_absent) { instance_double(ProductImport::ResetAbsent) } let(:settings) do instance_double( ProductImport::Settings, + data_for_stock_reset?: true, + reset_all_absent?: true, importing_into_inventory?: false, updated_ids: [1] ) end - before do - allow(ProductImport::Settings).to receive(:new) { settings } - allow(ProductImport::ResetAbsent) - .to receive(:new).and_return(reset_absent) + context 'when reset_absent_items was executed' do + let(:reset_absent) do + instance_double(ProductImport::ResetAbsent, call: 2) + end - allow(reset_absent).to receive(:products_reset_count) + before do + allow(ProductImport::Settings).to receive(:new) { settings } + allow(ProductImport::ResetAbsent).to receive(:new) { reset_absent } + end + + it 'returns the number of items affected by the last reset' do + entry_processor.reset_absent_items + expect(entry_processor.products_reset_count).to eq(2) + end end - it 'delegates to ResetAbsent' do - entry_processor.products_reset_count - expect(reset_absent).to have_received(:products_reset_count) + context 'when ResetAbsent was not called' do + it 'returns 0' do + expect(entry_processor.products_reset_count).to eq(0) + end end end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 14fab6de2c..07ef02ae45 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -21,8 +21,8 @@ module ProductImport instance_double(InventoryResetStrategy, supplier_ids: []) end - it 'returns nil' do - expect(reset_absent.call).to be_nil + it 'returns 0' do + expect(reset_absent.call).to eq(0) end end @@ -81,34 +81,10 @@ module ProductImport expect(reset_stock_strategy).not_to receive(:reset) reset_absent.call end - end - end - describe '#products_reset_count' do - let(:enterprise) { instance_double(Enterprise, id: 1) } - - let(:settings) do - instance_double( - Settings, - enterprises_to_reset: [enterprise.id.to_s] - ) - end - - let(:reset_stock_strategy) { instance_double(InventoryResetStrategy) } - - before do - allow(entry_processor) - .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } - - allow(reset_stock_strategy).to receive(:<<).with(enterprise.id) - allow(reset_stock_strategy) - .to receive(:supplier_ids) { [enterprise.id] } - allow(reset_stock_strategy).to receive(:reset) { 1 } - end - - it 'returns the number of reset variant overrides' do - reset_absent.call - expect(reset_absent.products_reset_count).to eq(1) + it 'returns 0' do + expect(reset_absent.call).to eq(0) + end end end end From 6a7359a3c5c1ff2f25d306d3d082f6c4f1351c89 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 1 Oct 2018 17:37:49 +0200 Subject: [PATCH 163/190] Pass supplier_ids in strategy #reset instead This removes the need to expose supplier_ids through #<< and makes both ResetAbsent and its strategies simpler. This could be made even simpler if the strategies just implemented `#relation` as public method and ResetAbsent called `#update_all` on them. The data to be fetched is the only thing that changes but the update is the same. --- .../inventory_reset_strategy.rb | 11 +- .../product_import/products_reset_strategy.rb | 11 +- app/models/product_import/reset_absent.rb | 20 +-- .../inventory_reset_strategy_spec.rb | 138 ++++++++++++++---- .../products_reset_strategy_spec.rb | 136 +++++++++++++---- .../product_import/reset_absent_spec.rb | 29 ++-- 6 files changed, 243 insertions(+), 102 deletions(-) diff --git a/app/models/product_import/inventory_reset_strategy.rb b/app/models/product_import/inventory_reset_strategy.rb index d89891e19f..c22cb6218a 100644 --- a/app/models/product_import/inventory_reset_strategy.rb +++ b/app/models/product_import/inventory_reset_strategy.rb @@ -1,17 +1,12 @@ module ProductImport class InventoryResetStrategy - attr_reader :supplier_ids - def initialize(excluded_items_ids) - @supplier_ids = [] @excluded_items_ids = excluded_items_ids end - def <<(values) - @supplier_ids << values - end + def reset(supplier_ids) + @supplier_ids = supplier_ids - def reset if supplier_ids.present? relation.update_all(count_on_hand: 0) else @@ -21,7 +16,7 @@ module ProductImport private - attr_reader :excluded_items_ids + attr_reader :excluded_items_ids, :supplier_ids def relation relation = VariantOverride.where(hub_id: supplier_ids) diff --git a/app/models/product_import/products_reset_strategy.rb b/app/models/product_import/products_reset_strategy.rb index bdb6fd7d5e..80dd6a448c 100644 --- a/app/models/product_import/products_reset_strategy.rb +++ b/app/models/product_import/products_reset_strategy.rb @@ -1,17 +1,12 @@ module ProductImport class ProductsResetStrategy - attr_reader :supplier_ids - def initialize(excluded_items_ids) - @supplier_ids = [] @excluded_items_ids = excluded_items_ids end - def <<(values) - @supplier_ids << values - end + def reset(supplier_ids) + @supplier_ids = supplier_ids - def reset if supplier_ids.present? relation.update_all(count_on_hand: 0) else @@ -21,7 +16,7 @@ module ProductImport private - attr_reader :excluded_items_ids + attr_reader :excluded_items_ids, :supplier_ids def relation relation = Spree::Variant diff --git a/app/models/product_import/reset_absent.rb b/app/models/product_import/reset_absent.rb index 5e8d7d6bb3..549ea1e7de 100644 --- a/app/models/product_import/reset_absent.rb +++ b/app/models/product_import/reset_absent.rb @@ -11,24 +11,20 @@ module ProductImport # # @return [Integer] number of items affected by the reset def call - settings.enterprises_to_reset.each do |enterprise_id| - next unless entry_processor.permission_by_id?(enterprise_id) - - reset_stock_strategy << enterprise_id.to_i - end - - reset_stock + reset_stock_strategy.reset(authorized_enterprises) end private attr_reader :settings, :reset_stock_strategy, :entry_processor - def reset_stock - if reset_stock_strategy.supplier_ids.present? - reset_stock_strategy.reset - else - 0 + # Returns the enterprises that have permissions to be reset + # + # @return [Array] array of Enterprise ids + def authorized_enterprises + settings.enterprises_to_reset.map do |enterprise_id| + next unless entry_processor.permission_by_id?(enterprise_id) + enterprise_id.to_i end end end diff --git a/spec/models/product_import/inventory_reset_strategy_spec.rb b/spec/models/product_import/inventory_reset_strategy_spec.rb index 7a86390871..011fe9f304 100644 --- a/spec/models/product_import/inventory_reset_strategy_spec.rb +++ b/spec/models/product_import/inventory_reset_strategy_spec.rb @@ -3,16 +3,6 @@ require 'spec_helper' describe ProductImport::InventoryResetStrategy do let(:inventory_reset) { described_class.new(excluded_items_ids) } - describe '#<<' do - let(:excluded_items_ids) { [] } - let(:supplier_ids) { 1 } - - it 'stores the specified supplier_ids' do - inventory_reset << supplier_ids - expect(inventory_reset.supplier_ids).to eq([supplier_ids]) - end - end - describe '#reset' do let(:supplier_ids) { enterprise.id } let(:enterprise) { variant.product.supplier } @@ -27,43 +17,131 @@ describe ProductImport::InventoryResetStrategy do ) end - before { inventory_reset << supplier_ids } - context 'when there are excluded_items_ids' do let(:excluded_items_ids) { [variant_override.id] } - it 'does not update the count_on_hand of the excluded items' do - inventory_reset.reset - expect(variant_override.reload.count_on_hand).to eq(10) + context 'and supplier_ids is []' do + let(:supplier_ids) { [] } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + inventory_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end end - it 'updates the count_on_hand of the non-excluded items' do - non_excluded_variant_override = create( - :variant_override, - count_on_hand: 3, - hub: enterprise, - variant: variant - ) - inventory_reset.reset - expect(non_excluded_variant_override.reload.count_on_hand).to eq(0) + context 'and supplier_ids is nil' do + let(:supplier_ids) { nil } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + inventory_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is set' do + it 'does not update the count_on_hand of the excluded items' do + inventory_reset.reset(supplier_ids) + expect(variant_override.reload.count_on_hand).to eq(10) + end + + it 'updates the count_on_hand of the non-excluded items' do + non_excluded_variant_override = create( + :variant_override, + count_on_hand: 3, + hub: enterprise, + variant: variant + ) + inventory_reset.reset(supplier_ids) + expect(non_excluded_variant_override.reload.count_on_hand).to eq(0) + end end end context 'when there are no excluded_items_ids' do let(:excluded_items_ids) { [] } - it 'sets all count_on_hand to 0' do - inventory_reset.reset - expect(variant_override.reload.count_on_hand).to eq(0) + context 'and supplier_ids is []' do + let(:supplier_ids) { [] } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + inventory_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is nil' do + let(:supplier_ids) { nil } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + inventory_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is set' do + it 'sets all count_on_hand to 0' do + inventory_reset.reset(supplier_ids) + expect(variant_override.reload.count_on_hand).to eq(0) + end end end context 'when excluded_items_ids is nil' do let(:excluded_items_ids) { nil } - it 'sets all count_on_hand to 0' do - inventory_reset.reset - expect(variant_override.reload.count_on_hand).to eq(0) + context 'and supplier_ids is []' do + let(:supplier_ids) { [] } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + inventory_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is nil' do + let(:supplier_ids) { nil } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + inventory_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is set' do + it 'sets all count_on_hand to 0' do + inventory_reset.reset(supplier_ids) + expect(variant_override.reload.count_on_hand).to eq(0) + end end end end diff --git a/spec/models/product_import/products_reset_strategy_spec.rb b/spec/models/product_import/products_reset_strategy_spec.rb index 47c8fb393d..ba5a33c8d1 100644 --- a/spec/models/product_import/products_reset_strategy_spec.rb +++ b/spec/models/product_import/products_reset_strategy_spec.rb @@ -3,57 +3,135 @@ require 'spec_helper' describe ProductImport::ProductsResetStrategy do let(:products_reset) { described_class.new(excluded_items_ids) } - describe '#<<' do - let(:excluded_items_ids) { [] } - let(:supplier_ids) { 1 } - - it 'stores the specified supplier_ids' do - products_reset << supplier_ids - expect(products_reset.supplier_ids).to eq([supplier_ids]) - end - end - describe '#reset' do let(:supplier_ids) { enterprise.id } let(:enterprise) { variant.product.supplier } let(:variant) { create(:variant, count_on_hand: 2) } - before { products_reset << supplier_ids } - context 'when there are excluded_items_ids' do let(:excluded_items_ids) { [variant.id] } - it 'does not update the count_on_hand of the excluded items' do - products_reset.reset - expect(variant.reload.count_on_hand).to eq(2) + context 'and supplier_ids is []' do + let(:supplier_ids) { [] } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + products_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end end - it 'updates the count_on_hand of the non-excluded items' do - non_excluded_variant = create( - :variant, - count_on_hand: 3, - product: variant.product - ) - products_reset.reset - expect(non_excluded_variant.reload.count_on_hand).to eq(0) + context 'and supplier_ids is nil' do + let(:supplier_ids) { nil } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + products_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is set' do + it 'does not update the count_on_hand of the excluded items' do + products_reset.reset(supplier_ids) + expect(variant.reload.count_on_hand).to eq(2) + end + + it 'updates the count_on_hand of the non-excluded items' do + non_excluded_variant = create( + :variant, + count_on_hand: 3, + product: variant.product + ) + products_reset.reset(supplier_ids) + expect(non_excluded_variant.reload.count_on_hand).to eq(0) + end end end context 'when there are no excluded_items_ids' do let(:excluded_items_ids) { [] } - it 'sets all count_on_hand to 0' do - products_reset.reset - expect(variant.reload.count_on_hand).to eq(0) + context 'and supplier_ids is []' do + let(:supplier_ids) { [] } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + products_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is nil' do + let(:supplier_ids) { nil } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + products_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is nil' do + it 'sets all count_on_hand to 0' do + products_reset.reset(supplier_ids) + expect(variant.reload.count_on_hand).to eq(0) + end end end context 'when excluded_items_ids is nil' do let(:excluded_items_ids) { nil } - it 'sets all count_on_hand to 0' do - products_reset.reset - expect(variant.reload.count_on_hand).to eq(0) + context 'and supplier_ids is []' do + let(:supplier_ids) { [] } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + products_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is nil' do + let(:supplier_ids) { nil } + let(:relation) do + instance_double(ActiveRecord::Relation, update_all: true) + end + + before { allow(VariantOverride).to receive(:where) { relation } } + + it 'does not update any DB record' do + products_reset.reset(supplier_ids) + expect(relation).not_to have_received(:update_all) + end + end + + context 'and supplier_ids is nil' do + it 'sets all count_on_hand to 0' do + products_reset.reset(supplier_ids) + expect(variant.reload.count_on_hand).to eq(0) + end end end end diff --git a/spec/models/product_import/reset_absent_spec.rb b/spec/models/product_import/reset_absent_spec.rb index 07ef02ae45..d642c67338 100644 --- a/spec/models/product_import/reset_absent_spec.rb +++ b/spec/models/product_import/reset_absent_spec.rb @@ -10,20 +10,21 @@ module ProductImport describe '#call' do context 'when there are no enterprises_to_reset' do - let(:settings) do - instance_double( - Settings, - enterprises_to_reset: [] - ) - end + let(:settings) { instance_double(Settings, enterprises_to_reset: []) } + let(:reset_stock_strategy) { instance_double(InventoryResetStrategy) } - let(:reset_stock_strategy) do - instance_double(InventoryResetStrategy, supplier_ids: []) + before do + allow(reset_stock_strategy).to receive(:reset).with([]) { 0 } end it 'returns 0' do expect(reset_absent.call).to eq(0) end + + it 'calls the strategy' do + reset_absent.call + expect(reset_stock_strategy).to have_received(:reset) + end end context 'when there are enterprises_to_reset' do @@ -42,10 +43,8 @@ module ProductImport allow(entry_processor) .to receive(:permission_by_id?).with(enterprise.id.to_s) { true } - allow(reset_stock_strategy).to receive(:<<).with(enterprise.id) allow(reset_stock_strategy) - .to receive(:supplier_ids) { [enterprise.id] } - allow(reset_stock_strategy).to receive(:reset) { 2 } + .to receive(:reset).with([enterprise.id]) { 2 } end it 'returns the number of products reset' do @@ -53,8 +52,8 @@ module ProductImport end it 'resets the products of the specified suppliers' do - expect(reset_stock_strategy).to receive(:reset) { 2 } reset_absent.call + expect(reset_stock_strategy).to have_received(:reset) end end @@ -74,12 +73,12 @@ module ProductImport allow(entry_processor) .to receive(:permission_by_id?).with(enterprise.id.to_s) { false } - allow(reset_stock_strategy).to receive(:supplier_ids) { [] } + allow(reset_stock_strategy).to receive(:reset).with([nil]) { 0 } end - it 'does not reset stock' do - expect(reset_stock_strategy).not_to receive(:reset) + it 'calls the strategy' do reset_absent.call + expect(reset_stock_strategy).to have_received(:reset) end it 'returns 0' do From 017e3d14dfcebf78e939d9dcf7368de3f8afd5ab Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 2 Oct 2018 11:10:21 +0100 Subject: [PATCH 164/190] Use variable colour assignment --- app/assets/stylesheets/admin/components/pagination.scss | 2 +- app/assets/stylesheets/admin/variables.css.scss | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/admin/components/pagination.scss b/app/assets/stylesheets/admin/components/pagination.scss index fd3705a6fd..05cbf34ca7 100644 --- a/app/assets/stylesheets/admin/components/pagination.scss +++ b/app/assets/stylesheets/admin/components/pagination.scss @@ -13,7 +13,7 @@ } &.disabled { - background-color: #ccc; + background-color: $disabled_button; cursor: default; } } diff --git a/app/assets/stylesheets/admin/variables.css.scss b/app/assets/stylesheets/admin/variables.css.scss index f8c12e0d4d..162ad93c6c 100644 --- a/app/assets/stylesheets/admin/variables.css.scss +++ b/app/assets/stylesheets/admin/variables.css.scss @@ -8,8 +8,10 @@ $warning-red: #da5354; $warning-orange: #da7f52; $medium-grey: #919191; $pale-blue: #cee1f4; +$light-grey: #ccc; $admin-table-border: $pale-blue; $modal-close-button-color: #de6060; $modal-close-button-hover-color: #bf4545; +$disabled-button: $light-grey; From 55d7d5d1e4876f35798cdfa63c94ea7148e095c4 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 2 Oct 2018 11:10:51 +0100 Subject: [PATCH 165/190] Rename #capture_path to #payment_capture_path --- app/serializers/api/admin/order_serializer.rb | 4 ++-- app/views/spree/admin/orders/index.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/serializers/api/admin/order_serializer.rb b/app/serializers/api/admin/order_serializer.rb index 7ea181da5d..e3207b30fd 100644 --- a/app/serializers/api/admin/order_serializer.rb +++ b/app/serializers/api/admin/order_serializer.rb @@ -2,7 +2,7 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer attributes :id, :number, :full_name, :email, :phone, :completed_at, :display_total attributes :show_path, :edit_path, :state, :payment_state, :shipment_state attributes :payments_path, :shipments_path, :ship_path, :ready_to_ship, :created_at - attributes :distributor_name, :special_instructions, :capture_path + attributes :distributor_name, :special_instructions, :payment_capture_path has_one :distributor, serializer: Api::Admin::IdSerializer has_one :order_cycle, serializer: Api::Admin::IdSerializer @@ -39,7 +39,7 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer spree_routes_helper.fire_admin_order_path(object, e: 'ship') end - def capture_path + def payment_capture_path pending_payment = object.pending_payments.first return '' unless object.payment_required? && pending_payment spree_routes_helper.fire_admin_order_payment_path(object, pending_payment.id, e: 'capture') diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 5aa6f161b8..5c8f0e8d14 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -68,8 +68,8 @@ %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{::order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t('.edit')} %div{'ng-if' => 'order.ready_to_ship'} %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{::order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.ship')} - %div{'ng-if' => 'order.capture_path'} - %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.capture')} + %div{'ng-if' => 'order.payment_capture_path'} + %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.payment_capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.capture')} .orders-loading{'ng-show' => 'RequestMonitor.loading'} .row From daafe73d81cfb94823f0f59a8466add23597790b Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Tue, 2 Oct 2018 15:11:13 +0100 Subject: [PATCH 166/190] Delete representative_view dependency, not used --- Gemfile | 1 - Gemfile.lock | 9 --------- 2 files changed, 10 deletions(-) diff --git a/Gemfile b/Gemfile index 2b33c60993..3613fe6104 100644 --- a/Gemfile +++ b/Gemfile @@ -46,7 +46,6 @@ gem 'aws-sdk' gem 'db2fog' gem 'andand' gem 'truncate_html' -gem 'representative_view' gem 'rabl' # AMS is pinned to 0.8.4 because 0.9.x is a complete re-write, as is 0.10.x diff --git a/Gemfile.lock b/Gemfile.lock index a63804e6bf..34cfa71fba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -622,14 +622,6 @@ GEM rdoc (3.12.2) json (~> 1.4) redcarpet (3.2.3) - representative (1.0.5) - activesupport (>= 2.2.2) - builder (>= 2.1.2) - i18n (>= 0.4.1) - nokogiri (>= 1.4.2) - representative_view (1.2.2) - actionpack (> 2.3.0, < 4.0.0) - representative (~> 1.0.2) roadie (3.0.1) css_parser (~> 1.3.4) nokogiri (~> 1.6.0) @@ -821,7 +813,6 @@ DEPENDENCIES rails (~> 3.2.22) rails-i18n (~> 3.0.0) redcarpet - representative_view roadie-rails (~> 1.0.3) roo (~> 2.7.0) roo-xls (~> 1.1.0) From 8a6da745a3bfa296dd867b8b4f40856c008ab08a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Tue, 2 Oct 2018 19:15:12 +0000 Subject: [PATCH 167/190] Bump rspec-retry from 0.5.6 to 0.6.1 Bumps [rspec-retry](https://github.com/noredink/rspec-retry) from 0.5.6 to 0.6.1. - [Release notes](https://github.com/noredink/rspec-retry/releases) - [Changelog](https://github.com/NoRedInk/rspec-retry/blob/master/changelog.md) - [Commits](https://github.com/noredink/rspec-retry/compare/v0.5.6...v0.6.1) Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9beb761a25..5f652bd42e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -664,8 +664,8 @@ GEM rspec-expectations (~> 3.7.0) rspec-mocks (~> 3.7.0) rspec-support (~> 3.7.0) - rspec-retry (0.5.6) - rspec-core (> 3.3, < 3.8) + rspec-retry (0.6.1) + rspec-core (> 3.3) rspec-support (3.7.1) rubocop (0.57.2) jaro_winkler (~> 1.5.1) From f99ed81863c76b21efb908f123fab2ea1f096f80 Mon Sep 17 00:00:00 2001 From: niko Date: Thu, 4 Oct 2018 11:46:38 +0200 Subject: [PATCH 168/190] Make 'Clear All' button work on order cycle page --- .../controllers/order_cycles_controller.js.coffee | 8 ++++++-- .../controllers/order_cycles_controller_spec.js.coffee | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee index 75f379856d..7ed549e525 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee @@ -3,8 +3,12 @@ angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, C $scope.columns = Columns.columns $scope.saveAll = -> OrderCycles.saveChanges($scope.order_cycles_form) $scope.ordersCloseAtLimit = -31 # days - $scope.involvingFilter = 0 - $scope.scheduleFilter = 0 + + $scope.resetSelectFilters = -> + $scope.scheduleFilter = 0 + $scope.involvingFilter = 0 + $scope.query = '' + $scope.resetSelectFilters() compileData = -> for schedule in $scope.schedules diff --git a/spec/javascripts/unit/admin/order_cycles/controllers/order_cycles_controller_spec.js.coffee b/spec/javascripts/unit/admin/order_cycles/controllers/order_cycles_controller_spec.js.coffee index 4c36884c0a..c66bc43dfa 100644 --- a/spec/javascripts/unit/admin/order_cycles/controllers/order_cycles_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/order_cycles/controllers/order_cycles_controller_spec.js.coffee @@ -72,3 +72,13 @@ describe "OrderCyclesCtrl", -> it "the RequestMonitor will not longer have a state of loading", -> expect(scope.RequestMonitor.loading).toBe false + + describe "filtering order cycles", -> + it "filters by and resets filter variables", -> + scope.query = "test" + scope.scheduleFilter = 1 + scope.involvingFilter = 1 + scope.resetSelectFilters() + expect(scope.query).toBe "" + expect(scope.scheduleFilter).toBe 0 + expect(scope.involvingFilter).toBe 0 \ No newline at end of file From 7a1cffb4b574ea24928544dea139b736151fdba3 Mon Sep 17 00:00:00 2001 From: 73VW Date: Thu, 4 Oct 2018 18:43:30 +0200 Subject: [PATCH 169/190] Updated build status badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f65b505a1c..c2b07601a7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/openfoodfoundation/openfoodnetwork.svg?branch=master)](https://travis-ci.org/openfoodfoundation/openfoodnetwork) +[![Build Status](https://semaphoreci.com/api/v1/openfoodfoundation/openfoodnetwork-2/branches/master/badge.svg)](https://semaphoreci.com/openfoodfoundation/openfoodnetwork-2) [![Code Climate](https://codeclimate.com/github/openfoodfoundation/openfoodnetwork.png)](https://codeclimate.com/github/openfoodfoundation/openfoodnetwork) [![View performance data on Skylight](https://badges.skylight.io/status/EiXQ6sSKij8y.svg)](https://oss.skylight.io/app/applications/EiXQ6sSKij8y) From da904c908d716d746a35e93660dbaadbcd64ef77 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sat, 6 Oct 2018 03:02:13 +0800 Subject: [PATCH 170/190] Fix product import date when not all variants have it --- app/models/spree/product_decorator.rb | 5 +--- spec/models/spree/product_spec.rb | 41 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 18220aaafd..cade1d34d7 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -177,10 +177,7 @@ Spree::Product.class_eval do # Get the most recent import_date of a product's variants def import_date - variants.map do |variant| - next if variant.import_date.blank? - variant.import_date - end.sort.last + variants.map(&:import_date).compact.max end # Build a product distribution for each distributor diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index f90de704e2..16f045b03c 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -714,4 +714,45 @@ module Spree end end end + + describe "product import" do + describe "finding the most recent import date of the variants" do + let!(:product) { create(:product) } + + let(:reference_time) { Time.zone.now.beginning_of_day } + + before do + product.reload + end + + context "when the variants do not have an import date" do + let!(:variant_a) { create(:variant, product: product, import_date: nil) } + let!(:variant_b) { create(:variant, product: product, import_date: nil) } + + it "returns nil" do + expect(product.import_date).to be_nil + end + end + + context "when some variants have import date and some do not" do + let!(:variant_a) { create(:variant, product: product, import_date: nil) } + let!(:variant_b) { create(:variant, product: product, import_date: reference_time - 1.hour) } + let!(:variant_c) { create(:variant, product: product, import_date: reference_time - 2.hour) } + + it "returns the most recent import date" do + expect(product.import_date).to eq(variant_b.import_date) + end + end + + context "when all variants have import date" do + let!(:variant_a) { create(:variant, product: product, import_date: reference_time - 2.hour) } + let!(:variant_b) { create(:variant, product: product, import_date: reference_time - 1.hour) } + let!(:variant_c) { create(:variant, product: product, import_date: reference_time - 3.hour) } + + it "returns the most recent import date" do + expect(product.import_date).to eq(variant_b.import_date) + end + end + end + end end From 574f031020f193e9db847f02f621fc794b06769f Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Fri, 5 Oct 2018 21:36:03 +0100 Subject: [PATCH 171/190] Fix typo in matomo optout iframe url in cookies_policy_helper --- engines/web/app/helpers/web/cookies_policy_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engines/web/app/helpers/web/cookies_policy_helper.rb b/engines/web/app/helpers/web/cookies_policy_helper.rb index 0fb86f7793..0274b2d6c7 100644 --- a/engines/web/app/helpers/web/cookies_policy_helper.rb +++ b/engines/web/app/helpers/web/cookies_policy_helper.rb @@ -9,7 +9,7 @@ module Web def matomo_iframe_src "#{Spree::Config.matomo_url}"\ - "/index.php?module=CoreAdminome&action=optOut"\ + "/index.php?module=CoreAdminHome&action=optOut"\ "&language=#{locale_language}"\ "&backgroundColor=&fontColor=222222&fontSize=16px&fontFamily=%22Roboto%22%2C%20Arial%2C%20sans-serif" end From 8b59943256d8e5e6f5e7efcd5dd79064836635b5 Mon Sep 17 00:00:00 2001 From: Transifex-Openfoodnetwork Date: Mon, 8 Oct 2018 16:58:01 +1100 Subject: [PATCH 172/190] Updating translations for config/locales/de_DE.yml --- config/locales/de_DE.yml | 231 +++++++++++++++++++++------------------ 1 file changed, 127 insertions(+), 104 deletions(-) diff --git a/config/locales/de_DE.yml b/config/locales/de_DE.yml index 162b55d464..c3bf6a6e68 100644 --- a/config/locales/de_DE.yml +++ b/config/locales/de_DE.yml @@ -55,6 +55,7 @@ de_DE: user_registrations: spree_user: signed_up_but_unconfirmed: "Eine Nachricht mit einem Bestätigungslink wurde an Ihre E-Mail-Adresse gesendet. Bitte öffnen Sie den Link, um Ihr Konto zu aktivieren." + unknown_error: "Beim Erstellen Ihres Kontos ist ein Fehler aufgetreten. Überprüfen Sie Ihre E-Mail-Adresse und versuchen Sie es erneut." failure: invalid: | Ungültige E-Mail-Adresse oder Passwort. @@ -78,7 +79,7 @@ de_DE: cancel_email: dear_customer: "Sehr geehrter Kunde," instructions: "Ihre Bestellung wurde storniert. Bitte bewahren Sie diese Stornierungsinformationen für Ihre Unterlagen auf." - order_summary_canceled: "Bestellübersicht [ABGESAGT]" + order_summary_canceled: "Bestellübersicht [STORNIERT]" subject: "Stornierung der Bestellung" subtotal: "Zwischensumme: %{subtotal}" total: "Bestellsumme: %{total}" @@ -89,9 +90,9 @@ de_DE: shipped_email: dear_customer: "Sehr geehrter Kunde," instructions: "Ihre Bestellung wurde versandt" - shipment_summary: "Sendungszusammenfassung" + shipment_summary: "Übersicht" subject: "Versandbenachrichtigung" - thanks: "Danke für Ihr Geschäft." + thanks: "Danke für Ihren Einkauf." track_information: "Tracking-Informationen: %{tracking}" track_link: "Tracking-Link: %{url}" subscription_mailer: @@ -244,7 +245,7 @@ de_DE: shop: Laden sku: Artikelnummer status_state: Status - tags: '"tags"' + tags: Stichwörter variant: Variante weight: Gewicht volume: Volumen @@ -270,7 +271,7 @@ de_DE: viewing: "Zeigt: %{current_view_name}" description: Beschreibung whats_this: Was ist das? - tag_has_rules: "Vorhandene Regeln für dieses \"tag\": %{num}" + tag_has_rules: "Vorhandene Regeln für dieses Stichwort: %{num}" has_one_rule: "hat eine Regel" has_n_rules: "hat %{num} Regel(n)" unsaved_confirm_leave: "Es gibt ungespeicherte Änderungen auf dieser Seite. Möchten Sie ohne Speichern fortfahren?" @@ -458,6 +459,7 @@ de_DE: conditional_blank: Kann nicht leer sein, wenn Einheitart leer ist no_product: hat keine Produkte in der Datenbank gefunden not_found: nicht in der Datenbank gefunden + not_updatable: kann über den Produktimport nicht auf bestehende Produkte aktualisiert werden blank: kann nicht leer sein products_no_permission: Sie sind nicht berechtigt, Produkte für dieses Unternehmen zu verwalten inventory_no_permission: Sie sind nicht berechtigt, Bestand für diesen Produzenten zu erstellen @@ -469,7 +471,7 @@ de_DE: choose_import_type: Wählen Sie die Importart import_into: Importart product_list: Produktliste - inventories: Bestände + inventories: Kataloge import: Importieren upload: Hochladen csv_templates: CSV Vorlage @@ -516,6 +518,8 @@ de_DE: inventory_to_reset: Bestehende Katalogeinträge werden auf null Bestand zurückgesetzt line: Zeile item_line: Artikelzeile + import_review: + not_updatable_tip: "Die folgenden Felder können nicht über den Bulk-Import für bestehende Produkte aktualisiert werden:" save_results: final_results: Endgültige Ergebnisse importieren products_created: Produkte erstellt @@ -556,9 +560,6 @@ de_DE: controls: back_to_my_inventory: Zurück zu meinem Katalog orders: - index: - capture: "Erfassung" - ship: "Liefern" invoice_email_sent: 'Rechnungs-E-Mail wurde gesendet' order_email_resent: 'Bestellungs-E-Mail wurde erneut gesendet' bulk_management: @@ -725,11 +726,11 @@ de_DE: by_default: Standardmäßig no_rules_yet: Es gelten noch keine Standardregeln add_new_button: '+ Fügen Sie eine neue Standardregel hinzu' - no_tags_yet: Für dieses Unternehmen sind noch keine "tags" vorhanden - no_rules_yet: Für dieses "tag" gelten noch keine Regeln - for_customers_tagged: 'Für Kunden mit dem "tag":' + no_tags_yet: Für dieses Unternehmen sind noch keine Stichwörter vorhanden + no_rules_yet: Für dieses Stichwort gelten noch keine Regeln + for_customers_tagged: 'Für Kunden mit dem Stichwort:' add_new_rule: '+ Neue Regel hinzufügen' - add_new_tag: '+ Neues "tag" hinzufügen' + add_new_tag: '+ Neues Stichwort hinzufügen' users: email_confirmation_notice_html: "E-Mail-Bestätigung steht aus. Wir haben eine Bestätigungs-E-Mail an %{email} gesendet." resend: Erneut senden @@ -805,12 +806,12 @@ de_DE: title: Neues Unternehmen back_link: Zurück zur Unternehmensliste remove_logo: - remove: "Entferne Bild" + remove: "Bild entfernen" removed_successfully: "Das Logo wurde erfolgreich entfernt" immediate_removal_warning: "Das Logo wird sofort nach der Bestätigung entfernt." remove_promo_image: - remove: "Entferne Bild" - removed_successfully: "Promo-Bild wurde erfolgreich entfernt" + remove: "Bild entfernen" + removed_successfully: "Werbebild wurde erfolgreich entfernt" immediate_removal_warning: "Das Werbebild wird sofort nach der Bestätigung entfernt." welcome: welcome_title: Willkommen im Open Food Network! @@ -845,10 +846,10 @@ de_DE: coordinator_fees: add: Koordinatorgebühr hinzufügen filters: - search_by_order_cycle_name: "Suche nach Bestellung Cycle name ..." - involving: "Involviert" - any_enterprise: "Jede Firma" - any_schedule: "Jeder Zeitplan" + search_by_order_cycle_name: "Suche nach Bestellzyklus ..." + involving: "Betrifft" + any_enterprise: "Alle Unternehmen" + any_schedule: "Alle Zeitpläne" form: incoming: Eingehend supplier: Anbieter @@ -857,8 +858,8 @@ de_DE: outgoing: Ausgehend distributor: Verteiler products: Produkte - tags: '"tags"' - add_a_tag: '"tag" hinzufügen' + tags: Stichwörter + add_a_tag: Stichwort hinzufügen delivery_details: Abhol- / Lieferinformationen debug_info: Debug-Informationen index: @@ -1057,7 +1058,7 @@ de_DE: enterprise_logo: destroy_attachment_does_not_exist: "Logo existiert nicht" enterprise_promo_image: - destroy_attachment_does_not_exist: "Promo-Bild existiert nicht" + destroy_attachment_does_not_exist: "Webebild existiert nicht" checkout: already_ordered: cart: "Warenkorb" @@ -1109,12 +1110,12 @@ de_DE: contact: "Kontakt" require_customer_login: "Dieser Laden ist nur für Kunden." require_login_html: "Bitte %{login}, wenn Sie bereits ein Konto haben. Andernfalls, %{register}, um Kunde zu werden." - require_customer_html: "Bitte %{contact} %{enterprise}, um Kunde zu werden." + require_customer_html: "Bitte kontaktieren Sie %{enterprise}, um Kunde zu werden." card_could_not_be_updated: Die Karte konnte nicht aktualisiert werden card_could_not_be_saved: Karte konnte nicht gespeichert werden spree_gateway_error_flash_for_checkout: "Bei den Zahlungsinformationen ist ein Problem aufgetreten: %{error}" invoice_billing_address: "Rechnungsadresse:" - invoice_column_tax: "GST" + invoice_column_tax: "Umsatzsteuer" invoice_column_price: "Preis" invoice_column_item: "Artikel" invoice_column_qty: "Menge" @@ -1123,13 +1124,13 @@ de_DE: invoice_column_price_with_taxes: "Gesamtpreis (inkl. Steuern)" invoice_column_price_without_taxes: "Gesamtpreis (zzgl. Steuern)" invoice_column_tax_rate: "Steuersatz" - invoice_tax_total: "Gesamtkosten:" + invoice_tax_total: "Umsatzsteuersumme:" tax_invoice: "Steuerrechnung" tax_total: "Steuersumme (%{rate}):" total_excl_tax: "Summe (ohne Steuern):" total_incl_tax: "Gesamt (Inkl. Steuern):" - abn: "ABN:" - acn: "ACN:" + abn: "USt-IdNr." + acn: "St.-Nr." invoice_issued_on: "Rechnung ausgestellt am:" order_number: "Rechnungsnummer:" date_of_transaction: "Datum der Transaktion:" @@ -1138,18 +1139,18 @@ de_DE: ticket_column_unit_price: "Stückpreis" ticket_column_total_price: "Gesamtpreis" menu_1_title: "Läden" - menu_1_url: "/ Geschäfte" + menu_1_url: "/ läden" menu_2_title: "Karte" - menu_2_url: "/Karte" + menu_2_url: "/karte" menu_3_title: "Erzeuger" - menu_3_url: "/ Produzenten" + menu_3_url: "/ erzeuger" menu_4_title: "Gruppen" - menu_4_url: "/ Gruppen" + menu_4_url: "/ gruppen" menu_5_title: "Über Uns" menu_5_url: "http://www.openfoodnetwork.org/" - menu_6_title: "Verbinde" + menu_6_title: "Verbinden" menu_6_url: "https://openfoodnetwork.org/au/connect/" - menu_7_title: "Lerne" + menu_7_title: "Mehr Erfahren" menu_7_url: "https://openfoodnetwork.org/au/learn/" logo: "Logo (640x130)" logo_mobile: "Handylogo (75x26)" @@ -1166,7 +1167,7 @@ de_DE: footer_email: "E-Mail:" footer_links_md: "Links" footer_about_url: "Über URL" - user_guide_link: "Benutzeranleitung Link" + user_guide_link: "Benutzerhandbuch-Link" name: Name first_name: Vorname last_name: Nachname @@ -1243,13 +1244,13 @@ de_DE: legal: cookies_policy: header: "Wie wir Cookies verwenden" - desc_part_1: "Cookies sind sehr kleine Textdateien, die beim Besuch einiger Websites auf Ihrem Computer gespeichert werden." + desc_part_1: "Cookies sind sehr kleine Textdateien, die beim Besuch mancher Websiten auf Ihrem Computer gespeichert werden." desc_part_2: "In OFN respektieren wir Ihre Privatsphäre. Wir verwenden nur die Cookies, die notwendig sind, um Ihnen den Service des Online-Kaufs / -Verkaufs von Lebensmitteln zu bieten. Wir verkaufen keine Ihrer Daten. Wir könnten Ihnen in Zukunft vorschlagen, einige Ihrer Daten zu teilen, um neue Commons-Dienste zu entwickeln, die für das Ökosystem nützlich sein könnten (wie Logistikdienstleistungen für kurze Nahrungsmittelsysteme), aber wir sind noch nicht dort, und wir werden es nicht ohne Ihr tun Genehmigung :-)" desc_part_3: "Wir verwenden Cookies hauptsächlich, um sich daran zu erinnern, wer Sie sind, wenn Sie sich bei dem Dienst anmelden oder sich die Artikel merken können, die Sie in Ihren Warenkorb legen, auch wenn Sie nicht eingeloggt sind \"Cookies akzeptieren\" nehmen wir an, dass Sie uns die Speicherung von Cookies erlauben, die für das Funktionieren der Website notwendig sind. Hier ist die Liste der von uns verwendeten Cookies!" essential_cookies: "Essentielle Kekse" essential_cookies_desc: "Die folgenden Cookies sind für den Betrieb unserer Website unbedingt erforderlich." essential_cookies_note: "Die meisten Cookies enthalten nur einen eindeutigen Bezeichner, aber keine anderen Daten. Ihre E-Mail-Adresse und Ihr Kennwort sind zum Beispiel niemals darin enthalten und werden nie veröffentlicht." - cookie_domain: "Festlegen von:" + cookie_domain: "Einstellen nach:" cookie_session_desc: "Wird verwendet, damit die Website sich zwischen den Seitenbesuchen an die Benutzer erinnern kann, z. B. an Artikel in Ihrem Einkaufswagen." cookie_consent_desc: "Wird verwendet, um den Status der Benutzerzustimmung zum Speichern von Cookies beizubehalten" cookie_remember_me_desc: "Wird verwendet, wenn der Benutzer die Website aufgefordert hat, sich an ihn zu erinnern. Dieser Cookie wird nach 12 Tagen automatisch gelöscht. Wenn Sie als Benutzer möchten, dass dieser Cookie gelöscht wird, müssen Sie sich nur abmelden. Wenn Sie nicht möchten, dass der Cookie auf Ihrem Computer installiert wird, sollten Sie das Kontrollkästchen \"An mich erinnern\" nicht aktivieren, wenn Sie sich anmelden." @@ -1283,7 +1284,6 @@ de_DE: cookies_policy_link: "Hinweise zu Cookies" cookies_accept_button: "Cookies akzeptieren" home_shop: Jetzt einkaufen - brandstory_headline: "Essen, ohne eigene Rechtspersönlichkeit." brandstory_intro: "Manchmal ist der beste Weg, das System zu reparieren, ein neues zu starten ..." brandstory_part1: "Wir beginnen von Grund auf. Mit Bauern und Züchtern, die bereit sind, ihre Geschichten stolz und wahrhaftig zu erzählen. Mit Händlern, die bereit sind, Menschen mit Produkten fair und ehrlich zu verbinden. Mit Käufern, die glauben, dass bessere wöchentliche Einkaufsentscheidungen die Welt ernsthaft verändern können." brandstory_part2: "Dann brauchen wir einen Weg, um es real zu machen. Ein Weg, jeden zu stärken, der Lebensmittel anbaut, verkauft und kauft. Ein Weg, um alle Geschichten zu erzählen, um die gesamte Logistik zu bewältigen. Eine Möglichkeit, Transaktionen jeden Tag in Transformation umzuwandeln." @@ -1308,24 +1308,24 @@ de_DE: stats_producers: "Nahrungsmittelproduzenten" stats_shops: "Lebensmittelgeschäfte" stats_shoppers: "Lebensmittelkäufer" - stats_orders: "Essen Bestellungen" + stats_orders: "Nahrungsmittelbestellungen" checkout_title: Kasse checkout_now: Zur Kasse - checkout_order_ready: Bestellung bereit für + checkout_order_ready: Bestellung bereit am checkout_hide: Ausblenden checkout_expand: Erweitern checkout_headline: "Ok, jetzt zur Kasse?" checkout_as_guest: "Als Gast zur Kasse" - checkout_details: "Deine Details" + checkout_details: "Ihre Details" checkout_billing: "Rechnungsinfo" checkout_default_bill_address: "Als Standard-Rechnungsadresse speichern" checkout_shipping: Versandinformation checkout_default_ship_address: "Als Standardversandadresse speichern" - checkout_method_free: Frei + checkout_method_free: ?? checkout_address_same: Lieferadresse wie Rechnungsadresse? - checkout_ready_for: "Bereit für:" - checkout_instructions: "Irgendwelche Kommentare oder spezielle Anweisungen?" - checkout_payment: Bezahlung + checkout_ready_for: "Bereit am:" + checkout_instructions: "Kommentare oder spezielle Anweisungen?" + checkout_payment: Zahlung checkout_send: Jetzt bestellen checkout_your_order: Ihre Bestellung checkout_cart_total: Warenkorb insgesamt @@ -1336,23 +1336,23 @@ de_DE: order_paid: BEZAHLT order_not_paid: NICHT BEZAHLT order_total: Gesamtbestellung - order_payment: "Bezahlen über:" + order_payment: "Bezahlen per:" order_billing_address: Rechnungsadresse order_delivery_on: Lieferung am order_delivery_address: Lieferadresse order_delivery_time: Lieferzeit - order_special_instructions: "Deine Noten:" + order_special_instructions: "Deine Notizen:" order_pickup_time: abholbereit - order_pickup_instructions: Sammlung Anweisungen - order_produce: Produzieren + order_pickup_instructions: Abholinformationen + order_produce: Produkte order_total_price: Total order_includes_tax: (inkl. Steuern) - order_payment_paypal_successful: Ihre Zahlung via PayPal wurde erfolgreich abgewickelt. + order_payment_paypal_successful: Ihre Zahlung mit PayPal wurde erfolgreich abgewickelt. order_hub_info: Hub-Info order_back_to_store: Zurück zum Laden order_back_to_cart: Zurück zum Warenkorb bom_tip: "Verwenden Sie diese Seite, um Produktmengen über mehrere Bestellungen hinweg zu ändern. Produkte können bei Bedarf auch komplett aus Bestellungen entfernt werden." - unsaved_changes_warning: "Nicht gespeicherte Änderungen sind vorhanden und gehen verloren, wenn Sie fortfahren." + unsaved_changes_warning: "Sie haben nicht gespeicherte Änderungen, die beim Fortfahren verloren gehen." unsaved_changes_error: "Felder mit roten Rahmen enthalten Fehler." products: "Produkte" products_in: "in %{oc}" @@ -1374,9 +1374,9 @@ de_DE: email_confirmation_notice_unexpected: "Sie haben diese Nachricht erhalten, weil Sie sich unter %{sitename} angemeldet haben oder von einer Person eingeladen wurden, die Sie wahrscheinlich kennen. Wenn Sie nicht verstehen, warum Sie diese E-Mail erhalten, schreiben Sie bitte an %{contact}." email_social: "Verbinde dich mit uns:" email_contact: "Schreiben Sie uns eine E-Mail:" - email_signoff: "Prost," + email_signoff: "Danke" email_signature: "%{sitename} Team" - email_confirm_customer_greeting: "Hallo %[name]," + email_confirm_customer_greeting: "Hallo %{name}," email_confirm_customer_intro_html: "Vielen Dank für ihren Einkauf bei %{distributor} !" email_confirm_customer_number_html: "Bestellbestätigung # %{number} " email_confirm_customer_details_html: "Hier sind Ihre Bestelldetails von %{distributor} :" @@ -1393,7 +1393,7 @@ de_DE: email_payment_paid: BEZAHLT email_payment_not_paid: NICHT BEZAHLT email_payment_summary: Zahlungsübersicht - email_payment_method: "Bezahlen über:" + email_payment_method: "Bezahlen per:" email_so_placement_intro_html: "Sie haben eine neue Bestellung mit %{distributor} " email_so_placement_details_html: "Hier sind die Details Ihrer Bestellung für %{distributor} :" email_so_placement_changes: "Leider waren nicht alle von Ihnen angeforderten Produkte verfügbar. Die von Ihnen angeforderten Originalmengen sind unten durchgestrichen." @@ -1446,11 +1446,11 @@ de_DE: shopping_contact_web: "Kontakt" shopping_contact_social: "Folgen" shopping_groups_part_of: "ist ein Teil von:" - shopping_producers_of_hub: "%{hub} Produzenten:" - enterprises_next_closing: "Nächster Auftrag schließt" + shopping_producers_of_hub: "Erzeuger bei%{hub}" + enterprises_next_closing: "Nächster Bestellschluß" enterprises_ready_for: "Fertig am" - enterprises_choose: "Wählen Sie, wann Sie Ihre Bestellung wünschen:" - maps_open: "Öffnen" + enterprises_choose: "Wählen Sie, wann Sie Ihre Bestellung wollen:" + maps_open: "Offen" maps_closed: "Geschlossen" hubs_buy: "Suche nach:" hubs_shopping_here: "Hier einkaufen" @@ -1790,9 +1790,9 @@ de_DE: enterprise_long_desc_placeholder: "Dies ist Ihre Chance, die Geschichte Ihres Unternehmens zu erzählen - was macht Sie anders und wundervoll? Wir empfehlen, Ihre Beschreibung auf unter 600 Zeichen oder 150 Wörter zu beschränken." enterprise_long_desc_length: "%{num} Zeichen / bis zu 600 empfohlen" enterprise_limit: Unternehmensgrenze - enterprise_abn: "ABN" + enterprise_abn: "USt-IdNr." enterprise_abn_placeholder: "z.B. 99 123 456 789" - enterprise_acn: "ACN" + enterprise_acn: "St.-Nr." enterprise_acn_placeholder: "z.B. 123 456 789" enterprise_tax_required: "Sie müssen eine Auswahl treffen." enterprise_final_step: "Letzter Schritt!" @@ -2077,30 +2077,30 @@ de_DE: report_header_shipping: Lieferung report_header_shipping_method: Lieferart report_header_shipping_instructions: Versand-Anweisungen - report_header_ship_street: Schiffsstraße - report_header_ship_street_2: Schiffsstraße 2 - report_header_ship_city: Schiffsstadt - report_header_ship_postcode: Postleitzahl senden - report_header_ship_state: Schiffsstaat - report_header_billing_street: Rechnungsstraße - report_header_billing_street_2: Rechnungsstraße 2 - report_header_billing_street_3: Rechnungsstraße 3 - report_header_billing_street_4: Rechnungsstraße 4 - report_header_billing_city: Abrechnungsstadt - report_header_billing_postcode: Rechnungs Postleitzahl - report_header_billing_state: Abrechnungs Zustand + report_header_ship_street: Liefer-Straße + report_header_ship_street_2: Liefer-Straße 2 + report_header_ship_city: Liefer-Stadt + report_header_ship_postcode: Liefer-Postleitzahl + report_header_ship_state: Liefer-Bundesland + report_header_billing_street: Rechnung-Straße + report_header_billing_street_2: Rechnung-Straße 2 + report_header_billing_street_3: Rechnung-Straße 3 + report_header_billing_street_4: Rechnung-Straße 4 + report_header_billing_city: Rechnung-Stadt + report_header_billing_postcode: Rechnung-Postleitzahl + report_header_billing_state: Rechnung-Bundesland report_header_incoming_transport: Eingehender Transport - report_header_special_instructions: spezielle Anweisungen + report_header_special_instructions: Besondere Anweisungen report_header_order_number: Bestellnummer report_header_date: Datum report_header_confirmation_date: Bestätigungsdatum - report_header_tags: '"tags"' - report_header_items: Artikel - report_header_items_total: "Artikel insgesamt %{currency_symbol}" + report_header_tags: Stichwörter + report_header_items: Produkte + report_header_items_total: "Produkte insgesamt %{currency_symbol}" report_header_taxable_items_total: "Steuerpflichtige Posten insgesamt (%{currency_symbol})" report_header_sales_tax: "Umsatzsteuer (%{currency_symbol})" report_header_delivery_charge: "Liefergebühr (%{currency_symbol})" - report_header_tax_on_delivery: "Versandkosten (%{currency_symbol})" + report_header_tax_on_delivery: "Steuer auf Versandkosten (%{currency_symbol})" report_header_tax_on_fees: "Steuer auf Gebühren (%{currency_symbol})" report_header_total_tax: "Gesamtsteuer (%{currency_symbol})" report_header_enterprise: Unternehmen @@ -2119,9 +2119,9 @@ de_DE: report_header_taxons: Taxonen report_header_supplier: Anbieter report_header_producer: Erzeuger - report_header_producer_suburb: Produzent Vorort + report_header_producer_suburb: Erzeuger Vorort report_header_unit: Einheit - report_header_group_buy_unit_quantity: Gruppe Kaufeinheitsmenge + report_header_group_buy_unit_quantity: Gruppen-Kaufeinheitsmenge report_header_cost: Kosten report_header_shipping_cost: Versandkosten report_header_curr_cost_per_unit: Curr. Kosten pro Einheit @@ -2132,25 +2132,25 @@ de_DE: report_header_price: Preis report_header_unit_size: Einheitsgröße report_header_distributor: Verteiler - report_header_distributor_address: Händleradresse - report_header_distributor_city: Händlerstadt - report_header_distributor_postcode: Postleitzahl des Vertriebspartners + report_header_distributor_address: Verteileradresse + report_header_distributor_city: Verteilerstadt + report_header_distributor_postcode: Verteilerpostleitzahl report_header_delivery_address: Lieferadresse report_header_delivery_postcode: Liefer-Postleitzahl report_header_bulk_unit_size: Bulk-Einheitsgröße report_header_weight: Gewicht report_header_sum_total: Gesamtsumme report_header_date_of_order: Datum der Bestellung - report_header_amount_owing: Geschuldeten Betrag + report_header_amount_owing: Offener Betrag report_header_amount_paid: Bezahlter Betrag report_header_units_required: Benötigte Einheiten report_header_remainder: Rest - report_header_order_date: Auftragsdatum - report_header_order_id: Auftragsnummer + report_header_order_date: Bestelldatum + report_header_order_id: Bestellnummer report_header_item_name: Artikelname - report_header_temp_controlled_items: Temp Kontrollierte Artikel? + report_header_temp_controlled_items: Temperaturkontrollierte Artikel? report_header_customer_name: Kundenname - report_header_customer_email: Kunden-eMail + report_header_customer_email: Kunden-E-Mail report_header_customer_phone: Kundentelefon report_header_customer_city: Kundenstadt report_header_payment_state: Zahlungsstatus @@ -2158,17 +2158,17 @@ de_DE: report_header_item_price: "Artikel (%{currency})" report_header_item_fees_price: "Artikel + Gebühren (%{currency})" report_header_admin_handling_fees: "Verwaltung und Handhabung (%{currency})" - report_header_ship_price: "Schiff (%{currency})" - report_header_pay_fee_price: "Gebühren (%{currency})" + report_header_ship_price: "Liefer (%{currency})" + report_header_pay_fee_price: "Gebühren bezahlen (%{currency})" report_header_total_price: "Gesamt (%{currency})" - report_header_product_total_price: "Produkt gesamt (%{currency})" + report_header_product_total_price: "Produkte gesamt (%{currency})" report_header_shipping_total_price: "Versand Gesamt (%{currency})" - report_header_outstanding_balance_price: "Guthaben (%{currency})" + report_header_outstanding_balance_price: "Saldo (%{currency})" report_header_eft_price: "EFT (%{currency})" report_header_paypal_price: "PayPal (%{currency})" report_header_sku: Artikelnummer - report_header_amount: Menge - report_header_balance: Gesamt + report_header_amount: Betrag + report_header_balance: Saldo report_header_total_cost: "Gesamtkosten" report_header_total_ordered: Insgesamt bestellt report_header_total_max: Gesamt max @@ -2176,11 +2176,11 @@ de_DE: report_header_sum_max_total: "Summe Max. Summe" report_header_total_excl_vat: "Summe exkl. Steuern (%{currency_symbol})" report_header_total_incl_vat: "Summe inkl. Steuern (%{currency_symbol})" - report_header_temp_controlled: TempControlled? - report_header_is_producer: Hersteller? + report_header_temp_controlled: Temperaturkontrolliert? + report_header_is_producer: Erzeuger? report_header_not_confirmed: Nicht bestätigt - report_header_gst_on_income: GST auf Einkommen - report_header_gst_free_income: GST Freies Einkommen + report_header_gst_on_income: Umsatzsteuer auf Einkommen + report_header_gst_free_income: Unbesteuertes Einkommen report_header_total_untaxable_produce: Total unversteuerbares Produkt (keine Steuer) report_header_total_taxable_produce: Gesamtsteuerpflichtiges Produkt (inklusive Steuern) report_header_total_untaxable_fees: Summe nicht steuerpflichtiger Gebühren (keine Steuern) @@ -2224,7 +2224,7 @@ de_DE: payment_methods: "Zahlungsarten" payment_method_fee: "Transaktionsgebühr" inventory_settings: "Katalogeinstellungen" - tag_rules: "\"tag\"-Regeln" + tag_rules: "Stichwort-Regeln" shop_preferences: "Ladeneinstellungen" enterprise_fee_whole_order: Ganze Bestellung enterprise_fee_by: "%{type} Gebühr von %{role} %{enterprise_name}" @@ -2305,21 +2305,21 @@ de_DE: invite: "Einladen" invite_title: "Laden Sie einen nicht registrierten Benutzer ein" tag_rule_help: - title: '"tag"-Regeln' + title: Stichwort-Regeln overview: Überblick overview_text: > - Mit "tag"-Regeln können Sie beschreiben, welche Elemente für welche + Mit StichwortßRegeln können Sie beschreiben, welche Elemente für welche Kunden sichtbar sind. Elemente können Versandarten, Zahlungsarten, Produkte und Bestellzyklen sein. by_default_rules: "\"Standardmäßig ...\" Regeln" by_default_rules_text: > Mit Standardregeln können Sie Elemente verbergen, sodass sie standardmäßig nicht sichtbar sind. Dieses Verhalten kann dann durch nicht standardmäßige - Regeln für Kunden mit bestimmten "tags" überschrieben werden. - customer_tagged_rules: "'Kunden mit \"tag\" ...' Regeln" + Regeln für Kunden mit bestimmten Stichwörtern überschrieben werden. + customer_tagged_rules: "'Kunden mit Stichwort ...' Regeln" customer_tagged_rules_text: > - Durch das Erstellen von Regeln für ein bestimmtes "tag" können Sie die - Standardregeln für bestimmte Kunden überschreiben. + Durch das Erstellen von Regeln für ein bestimmtes Stichwort können Sie + die Standardregeln für bestimmte Kunden überschreiben. panels: save: SPEICHERN saved: GERETTET @@ -2495,7 +2495,15 @@ de_DE: header: store: Geschäft admin: + product_properties: + index: + inherits_properties_checkbox_hint: "Vererben Eigenschaften von %{supplier}? (außer oben aufgehoben)" orders: + index: + capture: "Erfassung" + ship: "Liefern" + edit: "Bearbeiten" + next: "Weiter" invoice: issued_on: Ausgegeben am tax_invoice: Steuerrechnung @@ -2530,6 +2538,11 @@ de_DE: account_id: Konto-ID business_name: Geschäftsname charges_enabled: Gebühren aktiviert + payments: + source_forms: + stripe: + error_saving_payment: Fehler beim Speichern der Zahlung + submitting_payment: Zahlung wird gesendet ... products: new: title: 'Neues Produkt' @@ -2578,7 +2591,12 @@ de_DE: producer_name: Produzent general_settings: edit: + legal_settings: "Rechtliche Einstellungen" + cookies_consent_banner_toggle: "Zeigen Sie das Zustimmungsbanner für Cookies" + privacy_policy_url: "Datenschutz URL" enterprises_require_tos: "Unternehmen müssen die AGB akzeptieren" + cookies_policy_matomo_section: "Zeigen Sie den Matomo-Abschnitt auf der Cookie-Richtlinienseite an" + cookies_policy_ga_section: "Google Analytics-Abschnitt auf der Cookie-Richtlinienseite anzeigen" footer_tos_url: "AGB URL" checkout: payment: @@ -2593,6 +2611,8 @@ de_DE: js_format: 'JJ-MM-TT' inventory: Katalog orders: + edit: + login_to_view_order: "Bitte loggen Sie sich ein, um Ihre Bestellung anzuzeigen." bought: item: "Bereits in dieser Reihenfolge bestellt" order_mailer: @@ -2689,5 +2709,8 @@ de_DE: authorised_shops: Bevollmächtigte Läden authorised_shops_popover: Dies ist die Liste der Shops, die Ihre Standardkreditkarte für eventuell vorhandene Abonnements (dh wiederkehrende Bestellungen) belasten dürfen. Ihre Kartendetails werden sicher aufbewahrt und nicht an Ladenbesitzer weitergegeben. Sie werden immer benachrichtigt, wenn Sie belastet werden. saved_cards_popover: Dies ist die Liste der Karten, die Sie für spätere Verwendung gespeichert haben. Ihr "Standard" wird automatisch beim Abschließen einer Bestellung ausgewählt und kann von allen Geschäften belastet werden, die Sie dazu berechtigt haben (siehe rechts). + authorised_shops: + shop_name: "Laden Name" + allow_charges?: "Gebühren erlauben?" localized_number: invalid_format: hat ein ungültiges Format. Bitte gebe eine Nummer ein. From 37a7cceead56bfdf2e02a228ed59c8c2aceb12d9 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 8 Oct 2018 15:42:46 +0100 Subject: [PATCH 173/190] Fix bindings on angular orders data --- app/views/spree/admin/orders/index.html.haml | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 5c8f0e8d14..159ca01238 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -15,7 +15,7 @@ - content_for :table_filter do = render partial: 'filters' - + %table#listing_orders.index.responsive{width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } %colgroup %col{style: "width: 10%"} @@ -39,37 +39,37 @@ %tbody %tr{ng: {repeat: 'order in orders track by $index', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'"} %td.align-center - {{::order.distributor_name}} + {{order.distributor_name}} %td.align-center - = @show_only_completed ? '{{::order.completed_at}}' : '{{::order.created_at}}' + = @show_only_completed ? '{{order.completed_at}}' : '{{order.created_at}}' %td - %a{'ng-href' => '{{::order.show_path}}'} + %a{'ng-href' => '{{order.show_path}}'} {{order.number}} %div{'ng-if' => 'order.special_instructions'} %br - %span.icon-warning-sign{'ofn-with-tip' => "{{::order.special_instructions}}"} + %span.icon-warning-sign{'ofn-with-tip' => "{{order.special_instructions}}"} = t('.note') %td.align-center %span.state{'ng-class' => 'order.state'} {{'order_state.' + order.state | t}} %td.align-center %span.state{'ng-class' => 'order.payment_state', 'ng-if' => 'order.payment_state'} - %a{'ng-href' => '{{::order.payments_path}}' } + %a{'ng-href' => '{{order.payments_path}}' } {{'payment_states.' + order.payment_state | t}} %td.align-center %span.state{'ng-class' => 'order.shipment_state', 'ng-if' => 'order.shipment_state'} - %a{'ng-href' => '{{::order.shipments_path}}' } + %a{'ng-href' => '{{order.shipments_path}}' } {{'shipment_states.' + order.shipment_state | t}} %td - = mail_to "{{::order.email}}" + = mail_to "{{order.email}}" %td.align-center - %span{'ng-bind-html' => '::order.display_total'} + %span{'ng-bind-html' => 'order.display_total'} %td.actions - %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{::order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t('.edit')} + %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t('.edit')} %div{'ng-if' => 'order.ready_to_ship'} - %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{::order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.ship')} + %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.ship')} %div{'ng-if' => 'order.payment_capture_path'} - %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.payment_capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.capture')} + %a.icon-capture.icon_link.no-text{'ng-href' => '{{order.payment_capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.capture')} .orders-loading{'ng-show' => 'RequestMonitor.loading'} .row From 3ec8c12899b5ed5d9aa5d43e83b7399edd7b7266 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 8 Oct 2018 20:41:59 +0100 Subject: [PATCH 174/190] Update account setting spec for updating email address --- spec/features/consumer/account/settings_spec.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/features/consumer/account/settings_spec.rb b/spec/features/consumer/account/settings_spec.rb index 383d03a4be..64cdff0f05 100644 --- a/spec/features/consumer/account/settings_spec.rb +++ b/spec/features/consumer/account/settings_spec.rb @@ -18,7 +18,12 @@ feature "Account Settings", js: true do expect(page).to have_content I18n.t('spree.users.form.account_settings') fill_in 'user_email', with: 'new@email.com' - click_button I18n.t(:update) + expect do + click_button I18n.t(:update) + end.to send_confirmation_instructions + + sent_mail = ActionMailer::Base.deliveries.last + expect(sent_mail.to).to eq ['new@email.com'] expect(find(".alert-box.success").text.strip).to eq "#{I18n.t(:account_updated)} ×" user.reload From 8f5fd41c8bf2dd2bd7f1d963e9ce9006e633d3f5 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Mon, 8 Oct 2018 21:30:49 +0100 Subject: [PATCH 175/190] Update email confirmation address --- app/mailers/spree/user_mailer_decorator.rb | 8 +++++++- spec/models/spree/user_spec.rb | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/mailers/spree/user_mailer_decorator.rb b/app/mailers/spree/user_mailer_decorator.rb index 3921d3c648..dced31e7fd 100644 --- a/app/mailers/spree/user_mailer_decorator.rb +++ b/app/mailers/spree/user_mailer_decorator.rb @@ -13,8 +13,14 @@ Spree::UserMailer.class_eval do @contact = ContentConfig.footer_email subject = t('spree.user_mailer.confirmation_instructions.subject') - mail(to: user.email, + mail(to: confirmation_email_address, from: from_address, subject: subject) end + + private + + def confirmation_email_address + @user.pending_reconfirmation? ? @user.unconfirmed_email : @user.email + end end diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index db0af97d15..ae54a9eca0 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -75,8 +75,11 @@ describe Spree.user_class do create(:mail_method) expect do - create(:user, confirmation_sent_at: nil, confirmed_at: nil) + create(:user, email: 'new_user@example.com', confirmation_sent_at: nil, confirmed_at: nil) end.to send_confirmation_instructions + + sent_mail = ActionMailer::Base.deliveries.last + expect(sent_mail.to).to eq ['new_user@example.com'] end context "with the the same email as existing customers" do From 4cbc449a51ce4836152984938e1f161976b6a8fe Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Tue, 9 Oct 2018 16:54:08 +1100 Subject: [PATCH 176/190] Remove unused parallel_tests gem Reverts db47c01784df955cff00560f3c4b5454467025dd --- .rspec_parallel | 4 ---- Gemfile | 1 - Gemfile.lock | 3 --- config/database.yml | 2 +- 4 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 .rspec_parallel diff --git a/.rspec_parallel b/.rspec_parallel deleted file mode 100644 index cee31855e2..0000000000 --- a/.rspec_parallel +++ /dev/null @@ -1,4 +0,0 @@ ---format Fuubar ---format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log ---format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log ---tag ~performance diff --git a/Gemfile b/Gemfile index 78c9109b10..b0dbe9e9fb 100644 --- a/Gemfile +++ b/Gemfile @@ -142,7 +142,6 @@ group :development do gem 'guard-livereload' gem 'guard-rails' gem 'guard-rspec', '~> 4.7.3' - gem 'parallel_tests' gem 'rubocop', '>= 0.49.1' # 1.0.9 fixed openssl issues on macOS https://github.com/eventmachine/eventmachine/issues/602 diff --git a/Gemfile.lock b/Gemfile.lock index 87be4a4f6d..d2226b0368 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -551,8 +551,6 @@ GEM cocaine (~> 0.5.3) mime-types parallel (1.11.2) - parallel_tests (2.14.1) - parallel parser (2.5.1.0) ast (~> 2.4.0) paypal-sdk-core (0.2.10) @@ -809,7 +807,6 @@ DEPENDENCIES oj paper_trail (~> 3.0.8) paperclip - parallel_tests pg poltergeist (>= 1.16.0) pry-byebug (>= 3.4.3) diff --git a/config/database.yml b/config/database.yml index 7f55b6c499..d313dd74e4 100644 --- a/config/database.yml +++ b/config/database.yml @@ -10,7 +10,7 @@ development: test: adapter: postgresql encoding: unicode - database: open_food_network_test<%= ENV['TEST_ENV_NUMBER'] %> + database: open_food_network_test pool: 5 host: localhost username: ofn From 245d900b58d472c744846dec3d559a316b3d5662 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Tue, 9 Oct 2018 17:06:59 +1100 Subject: [PATCH 177/190] Remove unused CI scripts We used to have our own CI server, but now we are using Travis and Semaphore. We don't need these scripts any more. And since parallel_tests was removed in the previous commit, they are broken anyway. --- script/ci/includes.sh | 13 ------------- script/ci/run_js_tests.sh | 17 ----------------- script/ci/run_tests.sh | 21 --------------------- 3 files changed, 51 deletions(-) delete mode 100755 script/ci/run_js_tests.sh delete mode 100755 script/ci/run_tests.sh diff --git a/script/ci/includes.sh b/script/ci/includes.sh index bcb5662469..8981b76ee4 100644 --- a/script/ci/includes.sh +++ b/script/ci/includes.sh @@ -1,10 +1,3 @@ -function load_environment { - source /var/lib/jenkins/.rvm/environments/ruby-2.1.5 - if [ ! -f config/application.yml ]; then - ln -s application.yml.example config/application.yml - fi -} - function require_env_vars { for var in "$@"; do eval value=\$$var @@ -66,12 +59,6 @@ function get_ofn_commit { fi } -function checkout_ofn_commit { - OFN_COMMIT=`buildkite-agent meta-data get "openfoodnetwork:git:commit"` - echo "Checking out stored commit $OFN_COMMIT" - git checkout -qf "$OFN_COMMIT" -} - function drop_and_recreate_database { # Adapted from: http://stackoverflow.com/questions/12924466/capistrano-with-postgresql-error-database-is-being-accessed-by-other-users DB=$1 diff --git a/script/ci/run_js_tests.sh b/script/ci/run_js_tests.sh deleted file mode 100755 index cd274eaf47..0000000000 --- a/script/ci/run_js_tests.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -e - -echo "--- Loading environment" -source ./script/ci/includes.sh -load_environment -checkout_ofn_commit - -echo "--- Verifying branch is based on current master" -exit_unless_master_merged - -echo "--- Bundling" -bundle install - -echo "--- Running tests" -./script/karma run diff --git a/script/ci/run_tests.sh b/script/ci/run_tests.sh deleted file mode 100755 index efae0805a6..0000000000 --- a/script/ci/run_tests.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -set -e - -echo "--- Loading environment" -source ./script/ci/includes.sh -load_environment -checkout_ofn_commit - -echo "--- Verifying branch is based on current master" -exit_unless_master_merged - -echo "--- Bundling" -bundle install - -echo "--- Loading test database" -bundle exec rake db:drop db:create db:schema:load -bundle exec rake parallel:drop parallel:create parallel:load_schema - -echo "--- Running tests" -bundle exec rake parallel:spec From c2c492cd6b5e421901c2cfed2bef767ae61f8f65 Mon Sep 17 00:00:00 2001 From: Transifex-Openfoodnetwork Date: Tue, 9 Oct 2018 21:57:06 +1100 Subject: [PATCH 178/190] Updating translations for config/locales/es.yml --- config/locales/es.yml | 61 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/config/locales/es.yml b/config/locales/es.yml index 34cab89377..198942497a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -55,6 +55,7 @@ es: user_registrations: spree_user: signed_up_but_unconfirmed: "Se ha enviado un mensaje con un enlace de confirmación a tu dirección de correo electrónico. Abre el enlace para activar tu cuenta." + unknown_error: "Algo salió mal al crear tu cuenta. Comprueba tu dirección de correo electrónico y vuelve a intentarlo." failure: invalid: | Correo o contraseña inválidos. @@ -91,6 +92,7 @@ es: instructions: "Tu pedido ha sido enviado" shipment_summary: "Resumen de envío" subject: "Notificación de envío" + thanks: "Gracias por hacer negocios." track_information: "Información de seguimiento: %{tracking}" track_link: "Enlace de seguimiento: %{url}" subscription_mailer: @@ -457,6 +459,7 @@ es: conditional_blank: no puede estar en blanco si unit_type está en blanco no_product: no coincide con ningún producto en la base de datos not_found: no encontrado en la base de datos + not_updatable: No se puede actualizar sobre productos existentes a través de la importación de productos blank: no puede estar vacío products_no_permission: no tienes permiso para administrar productos para esta organización inventory_no_permission: no tienes permiso para crear inventario para esta productora @@ -494,24 +497,40 @@ es: options_and_defaults: Opciones de importación y valores predeterminados no_permission: no tienes permiso para administrar esta organización not_found: no se pudo encontrar la organización en la base de datos + no_name: Sin nombre blank_supplier: algunos productos tienen un nombre de proveedor en blanco reset_absent?: Restablecer productos ausentes + reset_absent_tip: Establezca el stock en cero para todos los productos existentes que no estén presentes en el archivo. + overwrite_all: Sobrescribir todo + overwrite_empty: Sobrescribir si está vacío default_stock: Establecer nivel de existencias default_tax_cat: Establecer categoría de impuestos default_shipping_cat: Establecer categoría de envío + default_available_date: Establecer fecha disponible + validation_overview: Resumen de validación de importación + entries_found: Entradas encontradas en el archivo importado entries_with_errors: Los artículos contienen errores y no se importarán products_to_create: Los productos se crearán products_to_update: Los productos se actualizarán inventory_to_create: Se crearán Artículos de inventario inventory_to_update: Los artículos de inventario se actualizarán + products_to_reset: Los productos existentes tendrán su stock restablecido a cero + inventory_to_reset: Los artículos de inventario existentes tendrán su stock restablecido a cero line: Línea item_line: Línea del artículo + import_review: + not_updatable_tip: "Los siguientes campos no se pueden actualizar mediante la importación masiva de productos existentes:" + fields_ignored: Estos campos se ignorarán cuando se guarden los productos importados. + entries_table: + not_updatable: Este campo no es actualizable mediante importación masiva en productos existentes save_results: final_results: Importar resultados finales products_created: Productos creados products_updated: Productos actualizados inventory_created: Artículos de inventario creados inventory_updated: Artículos de inventario actualizados + products_reset: Los productos tenían nivel de stock restablecido a cero + inventory_reset: Los artículos de inventario tenían el nivel de stock restablecido a cero all_saved: "Todos los artículos guardados con éxito" some_saved: "Artículos guardados con éxito" save_errors: Guardar errores @@ -544,9 +563,6 @@ es: controls: back_to_my_inventory: Volver a mi inventario orders: - index: - capture: "Captura" - ship: "Envío" invoice_email_sent: 'Se ha enviado correo electrónico con la factura.' order_email_resent: 'El correo electrónico del pedido se ha reenviado' bulk_management: @@ -835,6 +851,8 @@ es: filters: search_by_order_cycle_name: "Buscar por nombre del Ciclo de Pedido..." involving: "Involucrando" + any_enterprise: "Cualquier organización" + any_schedule: "Cualquier horario" form: incoming: Entrante supplier: Proveedora @@ -1001,6 +1019,8 @@ es: allowed_payment_method_types_tip: Solo se pueden usar métodos de pago en efectivo y Stripe en este momento credit_card: Tarjeta de crédito charges_not_allowed: Los cargos no están permitidos para esta consumidora + no_default_card: La consumidora no tiene tarjetas disponibles para cargar + card_ok: La consumidora tiene una tarjeta disponible para cargar loading_flash: loading: CARGANDO SUSCRIPCIONES review: @@ -1237,15 +1257,26 @@ es: cookie_remember_me_desc: "Se usa si el usuario ha solicitado que el sitio web lo recuerde. Esta cookie se elimina automáticamente después de 12 días. Si como usuario deseas que se elimine esa cookie, solo necesitas desconectarte. Si no deseas que la cookie se instale en tu computadora no debes marcar la casilla \"recordarme\" al iniciar sesión." cookie_openstreemap_desc: "Utilizado por nuestro amigo proveedor de mapeo de código abierto (OpenStreetMap) para garantizar que no recibas demasiadas solicitudes durante un período de tiempo determinado, para evitar el abuso de sus servicios." cookie_stripe_desc: "Datos recopilados por nuestro procesador de pagos Stripe para detectar fraudes https://stripe.com/cookies-policy/legal. No todas las tiendas usan Stripe como método de pago pero es una buena práctica evitar fraude aplicarlo a todas las páginas. Stripe probablemente crea una imagen de cuáles de nuestras páginas generalmente interactúan con su API y luego marca cualquier cosa inusual. Por lo tanto configurar la cookie Stripe tiene una función más amplia que la simple provisión de un método de pago a un usuario. Eliminarla podría afectar la seguridad del servicio en sí. Puede obtener más información acerca de Stripe y leer su política de privacidad en https://stripe.com/privacy." + statistics_cookies: "Cookies de estadísticas" statistics_cookies_desc: "Las siguientes no son estrictamente necesarias, pero ayudan a proporcionarle una mejor experiencia de usuario al permitirnos analizar el comportamiento del usuario, identificar qué funciones usa más o no, comprender los problemas de la experiencia del usuario, etc." statistics_cookies_analytics_desc_html: "Para recopilar y analizar los datos de uso de la plataforma utilizamos Google Analytics, ya que era el servicio predeterminado conectado con Spree (el software de código abierto de comercio electrónico en el que creamos) pero nuestra visión es cambiar a Matomo (ex Piwik, herramienta analítica de código abierto que cumple con GDPR y protege tu privacidad) tan pronto como podamos." statistics_cookies_matomo_desc_html: "Para recopilar y analizar los datos de uso de la plataforma, utilizamos Matomo (ex Piwik), una herramienta analítica de código abierto que cumple con GDPR y protege tu privacidad" statistics_cookies_matomo_optout: "¿Deseas excluirte de Matomo Analytics? No recopilamos ningún dato personal y Matomo nos ayuda a mejorar nuestro servicio, pero respetamos tu elección :-)" cookie_analytics_utma_desc: "Se usa para distinguir usuarios y sesiones. La cookie se crea cuando la biblioteca javascript se ejecuta y no existe ninguna cookie __utma existente. La cookie se actualiza cada vez que se envían datos a Google Analytics." cookie_analytics_utmt_desc: "Se usa para acelerar la tasa de solicitud." + cookie_analytics_utmb_desc: "Se utiliza para determinar nuevas sesiones / visitas. La cookie se crea cuando la librería javascript se ejecuta y no existe ninguna cookie __utmb existente. La cookie se actualiza cada vez que los datos se envían a Google Analytics." + cookie_analytics_utmc_desc: "No utilizado en ga.js. Establecer para la interoperabilidad con urchin.js. Históricamente, esta cookie funcionó junto con la cookie __utmb para determinar si el usuario estaba en una nueva sesión / visita." + cookie_analytics_utmz_desc: "Almacena la fuente de tráfico o la campaña que explica cómo el usuario llegó a su sitio. La cookie se crea cuando se ejecuta la librería javascript y se actualiza cada vez que se envían datos a Google Analytics." + cookie_matomo_basics_desc: "Matomo cookies de origen para recopilar estadísticas." + cookie_matomo_heatmap_desc: "Matomo Heatmap y sesión de grabación de cookies." + cookie_matomo_ignore_desc: "Cookie utilizada para excluir al usuario de ser rastreado." disabling_cookies_header: "Advertencia sobre la desactivación de cookies" + disabling_cookies_desc: "Como usuario, siempre puede permitir, bloquear o eliminar las cookies de Open Food Network o cualquier otra página web cuando lo desee a través del control de configuración de su navegador. Cada navegador tiene una operativa diferente. Aquí están los enlaces:" + disabling_cookies_note: "Pero tenga en cuenta que si elimina o modifica las cookies esenciales utilizadas por Open Food Network, el sitio web no funcionará, no podrá agregar nada a su carrito ni realizar pedidos, por ejemplo." cookies_banner: + cookies_usage: "Este sitio utiliza cookies para que su navegación sea fluida y segura, y para ayudarnos a comprender cómo lo usa para mejorar las funciones que ofrecemos." cookies_definition: "Las cookies son archivos de texto muy pequeños que se almacenan en tu ordenador cuando visitas algunos sitios web." + cookies_desc: "Utilizamos solo las cookies que son necesarias para ofrecerle el servicio de venta / compra de alimentos en línea. No vendemos ninguno de sus datos. Utilizamos cookies principalmente para recordar quién es usted si 'inicia sesión' en el servicio, o para poder recordar los artículos que puso en su carrito, incluso si no ha iniciado sesión. Si continúa navegando en el sitio web sin hacer clic en \"Aceptar cookies\", asumimos que nos da su consentimiento para almacenar las cookies que son esenciales para el funcionamiento del sitio web." cookies_policy_link_desc: "Si desea obtener más información, consulte nuestro" cookies_policy_link: "política de cookies" cookies_accept_button: "Aceptar cookies" @@ -2203,6 +2234,7 @@ es: content_configuration_pricing_table: "(TODO: tabla de precios)" content_configuration_case_studies: "(TODO: Casos de Estudio)" content_configuration_detail: "(TODO: Detalle)" + enterprise_name_error: "ya está en uso. Si esta es su organización y le gustaría reclamar la propiedad, o si desea negociar con esta organización, comuníquese con el gestor actual de este perfil al %{email}." enterprise_owner_error: "^ %{email} no está autorizado a tener más organizaciones (el límite es %{enterprise_limit})." enterprise_role_uniqueness_error: "^Este rol ya está presente." inventory_item_visibility_error: Debe ser verdadero o falso @@ -2262,6 +2294,7 @@ es: choose: Escoger resolve_errors: Resuelve los siguientes errores more_items: "+ %{count} Más" + default_card_updated: Tarjeta predeterminada actualizada admin: enterprise_limit_reached: "Has alcanzado el límite estándar de organizaciones por cuenta. Escriba a %{contact_email} si necesita aumentarlo." modals: @@ -2456,11 +2489,28 @@ es: my_account: "Mi cuenta" date: "Fecha" time: "Hora" + layouts: + admin: + header: + store: Almacenar admin: product_properties: index: inherits_properties_checkbox_hint: "¿Heredar propiedades desde %{supplier}? (a menos que sea anulado arriba)" orders: + index: + listing_orders: "Pedidos de listado" + new_order: "Nuevo pedido" + capture: "Captura" + ship: "Envío" + edit: "Editar" + note: "Nota" + first: "primero" + last: "Último" + previous: "Anterior" + next: "Siguiente" + loading: "Cargando" + no_orders_found: "No se encontraron pedidos" invoice: issued_on: Emitido el tax_invoice: FACTURA DE IMPUESTOS @@ -2499,6 +2549,7 @@ es: source_forms: stripe: error_saving_payment: Error al guardar el pago + submitting_payment: Enviando pago... products: new: title: 'Nuevo producto' @@ -2551,6 +2602,8 @@ es: cookies_consent_banner_toggle: "Mostrar el banner de consentimiento de cookies" privacy_policy_url: "Vínculo con la Política de privacidad" enterprises_require_tos: "Las organizaciones deben aceptar los Términos del Servicio" + cookies_policy_matomo_section: "Mostrar la sección de Matomo en la página de política de cookies" + cookies_policy_ga_section: "Mostrar la sección de Google Analytics en la página de la política de cookies" footer_tos_url: "URL de términos y servicios" checkout: payment: @@ -2565,6 +2618,8 @@ es: js_format: 'yy-mm-dd' inventory: Inventario orders: + edit: + login_to_view_order: "Por favor inicie sesión para ver su pedido." bought: item: "Pedido en este ciclo de pedido" order_mailer: From dafcd0ddc2921709973b9ccc9f4edcf21076fd0a Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Tue, 25 Sep 2018 13:19:51 +0100 Subject: [PATCH 179/190] Add per_page controls to admin orders index --- .../admin/orders/controllers/orders_controller.js.coffee | 8 +++++++- .../stylesheets/admin/components/per_page_controls.scss | 7 +++++++ .../spree/admin/orders_controller_decorator.rb | 5 +++-- app/views/spree/admin/orders/_per_page_controls.html.haml | 6 ++++++ app/views/spree/admin/orders/index.html.haml | 3 +++ config/locales/en.yml | 5 +++++ 6 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 app/assets/stylesheets/admin/components/per_page_controls.scss create mode 100644 app/views/spree/admin/orders/_per_page_controls.html.haml diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 96bfd63ab2..b2c1e692f0 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -3,8 +3,14 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $scope.pagination = Orders.pagination $scope.orders = Orders.all $scope.sortOptions = SortOptions + $scope.per_page_options = [ + {id: 15, name: t('js.admin.orders.index.per_page', results: 15)}, + {id: 50, name: t('js.admin.orders.index.per_page', results: 50)}, + {id: 100, name: t('js.admin.orders.index.per_page', results: 100)} + ] $scope.initialise = -> + $scope.per_page = 15 $scope.q = { completed_at_not_null: true } @@ -25,7 +31,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, 'q[order_cycle_id_in]': $scope['q']['order_cycle_id_in'], 'q[order_cycle_id_in]': $scope['q']['order_cycle_id_in'], 'q[s]': $scope.sorting || 'id desc', - per_page: $scope.per_page || 15, + per_page: $scope.per_page, page: page }) diff --git a/app/assets/stylesheets/admin/components/per_page_controls.scss b/app/assets/stylesheets/admin/components/per_page_controls.scss new file mode 100644 index 0000000000..34ae9f7737 --- /dev/null +++ b/app/assets/stylesheets/admin/components/per_page_controls.scss @@ -0,0 +1,7 @@ +.per-page { + float: left; + + .per-page-feedback { + margin-left: 1em; + } +} diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index 6df8943966..88110da52f 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -65,8 +65,9 @@ Spree::Admin::OrdersController.class_eval do orders: ActiveModel::ArraySerializer.new(@orders, each_serializer: Api::Admin::OrderSerializer), pagination: { results: @orders.total_count, - pages: @orders.num_pages, - page: params[:page].to_i + pages: @orders.num_pages.to_i, + page: params[:page].to_i, + per_page: params[:per_page].to_i } } end diff --git a/app/views/spree/admin/orders/_per_page_controls.html.haml b/app/views/spree/admin/orders/_per_page_controls.html.haml new file mode 100644 index 0000000000..7a0c3fd081 --- /dev/null +++ b/app/views/spree/admin/orders/_per_page_controls.html.haml @@ -0,0 +1,6 @@ +.per-page{'ng-show' => '!RequestMonitor.loading && orders.length > 0'} + %input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'ng-model' => 'per_page'} + + %span.per-page-feedback + {{ 'spree.admin.orders.index.results_found' | t:{number: pagination.results} }} + {{ 'spree.admin.orders.index.viewing' | t:{start: ((pagination.page -1) * pagination.per_page) +1, end: ((pagination.page -1) * pagination.per_page) + orders.length} }} diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 159ca01238..1dc35e308a 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -16,6 +16,9 @@ - content_for :table_filter do = render partial: 'filters' +.row + = render partial: 'per_page_controls' + %table#listing_orders.index.responsive{width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } %colgroup %col{style: "width: 10%"} diff --git a/config/locales/en.yml b/config/locales/en.yml index ea6a746d97..be7e43f7b5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2571,6 +2571,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using resolve: Resolve new_tag_rule_dialog: select_rule_type: "Select a rule type:" + orders: + index: + per_page: "%{results} per page" resend_user_email_confirmation: resend: "Resend" sending: "Resend..." @@ -2667,6 +2670,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using next: "Next" loading: "Loading" no_orders_found: "No Orders Found" + results_found: "%{number} Results found." + viewing: "Viewing %{start} to %{end}." invoice: issued_on: Issued on tax_invoice: TAX INVOICE From 2d60b3180e71a5ab08b4a38269e2d6d3368a47c8 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 4 Oct 2018 17:44:34 +0200 Subject: [PATCH 180/190] Wrap and improve comment block readability --- app/models/spree/product_set.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/models/spree/product_set.rb b/app/models/spree/product_set.rb index 1a73ab00fe..368be9a556 100644 --- a/app/models/spree/product_set.rb +++ b/app/models/spree/product_set.rb @@ -3,11 +3,19 @@ class Spree::ProductSet < ModelSet super(Spree::Product, [], attributes, proc { |attrs| attrs[:product_id].blank? }) end - # A separate method of updating products was required due to an issue with the way Rails' assign_attributes and updates_attributes behave when delegated attributes of a nested - # object are updated via the parent object (ie. price of variants). Updating such attributes by themselves did not work using: - # product.update_attributes( { variants_attributes: [ { id: y, price: xx.x } ] } ) - # and so an explicit call to update attributes on each individual variant was required. ie: - # variant.update_attributes( { price: xx.x } ) + # A separate method of updating products was required due to an issue with + # the way Rails' assign_attributes and updates_attributes behave when + # delegated attributes of a nested object are updated via the parent object + # (ie. price of variants). Updating such attributes by themselves did not + # work using: + # + # product.update_attributes(variants_attributes: [{ id: y, price: xx.x }]) + # + # and so an explicit call to update attributes on each individual variant was + # required. ie: + # + # variant.update_attributes( { price: xx.x } ) + # def update_attributes(attributes) attributes[:taxon_ids] = attributes[:taxon_ids].split(',') if attributes[:taxon_ids].present? e = @collection.detect { |e| e.id.to_s == attributes[:id].to_s && !e.id.nil? } From f54c69cbbab37caa0d3e448a01daa8981a7f7825 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 4 Oct 2018 18:21:53 +0200 Subject: [PATCH 181/190] Add first test case for ProductSet This covers creation and update of a product. --- spec/models/spree/product_set_spec.rb | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 spec/models/spree/product_set_spec.rb diff --git a/spec/models/spree/product_set_spec.rb b/spec/models/spree/product_set_spec.rb new file mode 100644 index 0000000000..543debb0de --- /dev/null +++ b/spec/models/spree/product_set_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Spree::ProductSet do + describe '#save' do + context 'when passing :collection_attributes' do + let(:product_set) do + described_class.new(collection_attributes: collection_hash) + end + + context 'when the product does not exist yet' do + let(:collection_hash) do + { + 0 => { + product_id: 11, + name: 'a product', + price: 2.0, + supplier_id: create(:enterprise).id, + primary_taxon_id: create(:taxon).id, + unit_description: 'description', + variant_unit: 'items', + variant_unit_name: 'bunches' + } + } + end + + it 'creates it with the specified attributes' do + product_set.save + + expect(Spree::Product.last.attributes) + .to include('name' => 'a product') + end + end + + context 'when the product does exist' do + let!(:product) do + create( + :simple_product, + variant_unit: 'items', + variant_unit_scale: nil, + variant_unit_name: 'bunches' + ) + end + + let(:collection_hash) do + { + 0 => { + id: product.id, + variant_unit: 'weight', + variant_unit_scale: 1 + } + } + end + + it 'updates all the specified product attributes' do + product_set.save + + expect(product.reload.attributes).to include( + 'variant_unit' => 'weight', + 'variant_unit_scale' => 1 + ) + end + end + end + end +end From d43726504b5dbf23da780a3dd0d087c8781fd1ca Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 4 Oct 2018 19:59:26 +0200 Subject: [PATCH 182/190] Make #update_attributes parseable by humans As it is this is impossible to follow. --- app/models/spree/product_set.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/models/spree/product_set.rb b/app/models/spree/product_set.rb index 368be9a556..3a40ea635d 100644 --- a/app/models/spree/product_set.rb +++ b/app/models/spree/product_set.rb @@ -17,14 +17,20 @@ class Spree::ProductSet < ModelSet # variant.update_attributes( { price: xx.x } ) # def update_attributes(attributes) - attributes[:taxon_ids] = attributes[:taxon_ids].split(',') if attributes[:taxon_ids].present? - e = @collection.detect { |e| e.id.to_s == attributes[:id].to_s && !e.id.nil? } - if e.nil? + if attributes[:taxon_ids].present? + attributes[:taxon_ids] = attributes[:taxon_ids].split(',') + end + + found_model = @collection.find do |model| + model.id.to_s == attributes[:id].to_s && model.persisted? + end + + if found_model.nil? @klass.new(attributes).save unless @reject_if.andand.call(attributes) else - ( attributes.except(:id, :variants_attributes, :master_attributes).present? ? e.update_attributes(attributes.except(:id, :variants_attributes, :master_attributes)) : true) and - (attributes[:variants_attributes] ? update_variants_attributes(e, attributes[:variants_attributes]) : true ) and - (attributes[:master_attributes] ? update_variant(e, attributes[:master_attributes]) : true ) + ( attributes.except(:id, :variants_attributes, :master_attributes).present? ? found_model.update_attributes(attributes.except(:id, :variants_attributes, :master_attributes)) : true) and + (attributes[:variants_attributes] ? update_variants_attributes(found_model, attributes[:variants_attributes]) : true ) and + (attributes[:master_attributes] ? update_variant(found_model, attributes[:master_attributes]) : true ) end end From 575d76e23e03fb40c59f80edf47faa71213ec0c1 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 4 Oct 2018 20:25:24 +0200 Subject: [PATCH 183/190] Cover variant creation and update with basic tests --- spec/models/spree/product_set_spec.rb | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/models/spree/product_set_spec.rb b/spec/models/spree/product_set_spec.rb index 543debb0de..a53b9817e1 100644 --- a/spec/models/spree/product_set_spec.rb +++ b/spec/models/spree/product_set_spec.rb @@ -59,6 +59,36 @@ describe Spree::ProductSet do 'variant_unit_scale' => 1 ) end + + context 'when :master_attributes is passed' do + let(:master_attributes) { { sku: '123' } } + + before do + collection_hash[0][:master_attributes] = master_attributes + end + + context 'and the variant does exist' do + let!(:variant) { create(:variant, product: product) } + + before { master_attributes[:id] = variant.id } + + it 'updates the attributes of the master variant' do + product_set.save + expect(variant.reload.sku).to eq('123') + end + end + + context 'and the variant does not exist' do + let(:master_attributes) do + attributes_for(:variant).merge(sku: '123') + end + + it 'creates it with the specified attributes' do + product_set.save + expect(Spree::Variant.last.sku).to eq('123') + end + end + end end end end From a2228d4131aaf80639df9f67ce276dddeb91e53f Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 4 Oct 2018 20:26:49 +0200 Subject: [PATCH 184/190] Make ProductSet parseable by humans Now it's imposible to understand what is really going on. Feels more like assembler than Ruby. --- app/models/model_set.rb | 7 +++-- app/models/spree/product_set.rb | 46 +++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/app/models/model_set.rb b/app/models/model_set.rb index b73c15f7df..0236a00776 100644 --- a/app/models/model_set.rb +++ b/app/models/model_set.rb @@ -30,8 +30,11 @@ class ModelSet def errors errors = ActiveModel::Errors.new self - full_messages = @collection.map { |ef| ef.errors.full_messages }.flatten - full_messages.each { |fm| errors.add(:base, fm) } + full_messages = @collection + .map { |model| model.errors.full_messages } + .flatten + + full_messages.each { |message| errors.add(:base, message) } errors end diff --git a/app/models/spree/product_set.rb b/app/models/spree/product_set.rb index 3a40ea635d..db5fbfda2c 100644 --- a/app/models/spree/product_set.rb +++ b/app/models/spree/product_set.rb @@ -28,9 +28,35 @@ class Spree::ProductSet < ModelSet if found_model.nil? @klass.new(attributes).save unless @reject_if.andand.call(attributes) else - ( attributes.except(:id, :variants_attributes, :master_attributes).present? ? found_model.update_attributes(attributes.except(:id, :variants_attributes, :master_attributes)) : true) and - (attributes[:variants_attributes] ? update_variants_attributes(found_model, attributes[:variants_attributes]) : true ) and - (attributes[:master_attributes] ? update_variant(found_model, attributes[:master_attributes]) : true ) + update_product_only_attributes(found_model, attributes) && + update_product_variants(found_model, attributes) && + update_product_master(found_model, attributes) + end + end + + def update_product_only_attributes(product, attributes) + if attributes.except(:id, :variants_attributes, :master_attributes).present? + product.update_attributes( + attributes.except(:id, :variants_attributes, :master_attributes) + ) + else + true + end + end + + def update_product_variants(product, attributes) + if attributes[:variants_attributes] + update_variants_attributes(product, attributes[:variants_attributes]) + else + true + end + end + + def update_product_master(product, attributes) + if attributes[:master_attributes] + update_variant(product, attributes[:master_attributes]) + else + true end end @@ -41,16 +67,20 @@ class Spree::ProductSet < ModelSet end def update_variant(product, variant_attributes) - e = product.variants_including_master.detect { |e| e.id.to_s == variant_attributes[:id].to_s && !e.id.nil? } - if e.present? - e.update_attributes(variant_attributes.except(:id)) + found_variant = product.variants_including_master.find do |variant| + variant.id.to_s == variant_attributes[:id].to_s && variant.persisted? + end + + if found_variant.present? + found_variant.update_attributes(variant_attributes.except(:id)) else - product.variants.create variant_attributes + product.variants.create(variant_attributes) end end def collection_attributes=(attributes) - @collection = Spree::Product.where( :id => attributes.each_value.map{ |p| p[:id] } ) + @collection = Spree::Product + .where(id: attributes.each_value.map { |product| product[:id] }) @collection_hash = attributes end From cbac916e668e075e049b4f967c8d5479aca67216 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 8 Oct 2018 16:45:37 +0200 Subject: [PATCH 185/190] Validate unit value when updating variant_unit Variants whose product's variant_unit is weight or volume require a unit_value. --- app/models/spree/product_set.rb | 21 ++++- .../spree/admin/products_controller_spec.rb | 77 ++++++++++++++++--- spec/models/spree/product_set_spec.rb | 59 ++++++++------ 3 files changed, 119 insertions(+), 38 deletions(-) diff --git a/app/models/spree/product_set.rb b/app/models/spree/product_set.rb index db5fbfda2c..a681f689f3 100644 --- a/app/models/spree/product_set.rb +++ b/app/models/spree/product_set.rb @@ -35,15 +35,28 @@ class Spree::ProductSet < ModelSet end def update_product_only_attributes(product, attributes) - if attributes.except(:id, :variants_attributes, :master_attributes).present? - product.update_attributes( - attributes.except(:id, :variants_attributes, :master_attributes) - ) + variant_related_attrs = [:id, :variants_attributes, :master_attributes] + + if attributes.except(*variant_related_attrs).present? + product.assign_attributes(attributes.except(*variant_related_attrs)) + + product.variants.each do |variant| + validate_presence_of_unit_value(product, variant) + end + + product.save if errors.empty? else true end end + def validate_presence_of_unit_value(product, variant) + return unless %w(weight volume).include?(product.variant_unit) + return if variant.unit_value.present? + + product.errors.add(:unit_value, "can't be blank") + end + def update_product_variants(product, attributes) if attributes[:variants_attributes] update_variants_attributes(product, attributes[:variants_attributes]) diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb index d5b1f861b5..35fdf054c4 100644 --- a/spec/controllers/spree/admin/products_controller_spec.rb +++ b/spec/controllers/spree/admin/products_controller_spec.rb @@ -1,22 +1,75 @@ require 'spec_helper' describe Spree::Admin::ProductsController, type: :controller do - describe "updating a product we do not have access to" do - let(:s_managed) { create(:enterprise) } - let(:s_unmanaged) { create(:enterprise) } - let(:p) { create(:simple_product, supplier: s_unmanaged, name: 'Peas') } + describe 'bulk_update' do + context "updating a product we do not have access to" do + let(:s_managed) { create(:enterprise) } + let(:s_unmanaged) { create(:enterprise) } + let(:product) do + create(:simple_product, supplier: s_unmanaged, name: 'Peas') + end - before do - login_as_enterprise_user [s_managed] - spree_post :bulk_update, {"products" => [{"id" => p.id, "name" => "Pine nuts"}]} + before do + login_as_enterprise_user [s_managed] + spree_post :bulk_update, { + "products" => [{"id" => product.id, "name" => "Pine nuts"}] + } + end + + it "denies access" do + response.should redirect_to spree.unauthorized_url + end + + it "does not update any product" do + product.reload.name.should_not == "Pine nuts" + end end - it "denies access" do - response.should redirect_to spree.unauthorized_url - end + context "when changing a product's variant_unit" do + let(:producer) { create(:enterprise) } + let!(:product) do + create( + :simple_product, + supplier: producer, + variant_unit: 'items', + variant_unit_scale: nil, + variant_unit_name: 'bunches', + unit_value: nil, + unit_description: 'some description' + ) + end - it "does not update any product" do - p.reload.name.should_not == "Pine nuts" + before { login_as_enterprise_user([producer]) } + + it 'fails' do + spree_post :bulk_update, { + "products" => [ + { + "id" => product.id, + "variant_unit" => "weight", + "variant_unit_scale" => 1 + } + ] + } + + expect(response).to have_http_status(400) + end + + it 'does not redirect to bulk_products' do + spree_post :bulk_update, { + "products" => [ + { + "id" => product.id, + "variant_unit" => "weight", + "variant_unit_scale" => 1 + } + ] + } + + expect(response).not_to redirect_to( + '/api/products/bulk_products?page=1;per_page=500;' + ) + end end end diff --git a/spec/models/spree/product_set_spec.rb b/spec/models/spree/product_set_spec.rb index a53b9817e1..7f03f594f3 100644 --- a/spec/models/spree/product_set_spec.rb +++ b/spec/models/spree/product_set_spec.rb @@ -32,35 +32,50 @@ describe Spree::ProductSet do end context 'when the product does exist' do - let!(:product) do - create( - :simple_product, - variant_unit: 'items', - variant_unit_scale: nil, - variant_unit_name: 'bunches' - ) - end + context 'when a different varian_unit is passed' do + let!(:product) do + create( + :simple_product, + variant_unit: 'items', + variant_unit_scale: nil, + variant_unit_name: 'bunches', + unit_value: nil, + unit_description: 'some description' + ) + end - let(:collection_hash) do - { - 0 => { - id: product.id, - variant_unit: 'weight', - variant_unit_scale: 1 + let(:collection_hash) do + { + 0 => { + id: product.id, + variant_unit: 'weight', + variant_unit_scale: 1 + } } - } - end + end - it 'updates all the specified product attributes' do - product_set.save + it 'does not update the product' do + product_set.save - expect(product.reload.attributes).to include( - 'variant_unit' => 'weight', - 'variant_unit_scale' => 1 - ) + expect(product.reload.attributes).to include( + 'variant_unit' => 'items' + ) + end + + it 'adds an error' do + product_set.save + expect(product_set.errors.get(:base)) + .to include("Unit value can't be blank") + end + + it 'returns false' do + expect(product_set.save).to eq(false) + end end context 'when :master_attributes is passed' do + let!(:product) { create(:simple_product) } + let(:collection_hash) { { 0 => { id: product.id } } } let(:master_attributes) { { sku: '123' } } before do From c8c16f0e8a5f411cd694dc0b7510358b3d578062 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 8 Oct 2018 16:54:22 +0200 Subject: [PATCH 186/190] Use Rails 3.2 validates syntax --- app/models/spree/variant_decorator.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 4c7e6ed5ab..cefb7917da 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -18,11 +18,13 @@ Spree::Variant.class_eval do attr_accessible :unit_value, :unit_description, :images_attributes, :display_as, :display_name, :import_date accepts_nested_attributes_for :images - validates_presence_of :unit_value, - if: -> v { %w(weight volume).include? v.product.andand.variant_unit } + validates :unit_value, presence: true, if: -> (variant) { + %w(weight volume).include?(variant.product.andand.variant_unit) + } - validates_presence_of :unit_description, - if: -> v { v.product.andand.variant_unit.present? && v.unit_value.nil? } + validates :unit_description, presence: true, if: -> (variant) { + variant.product.andand.variant_unit.present? && variant.unit_value.nil? + } before_validation :update_weight_from_unit_value, if: -> v { v.product.present? } after_save :update_units From 5bd375d422463fbc3a7104a78a57529568daceef Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 8 Oct 2018 21:23:08 +0200 Subject: [PATCH 187/190] Favor early return over dumb else branch --- app/models/spree/product_set.rb | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/app/models/spree/product_set.rb b/app/models/spree/product_set.rb index a681f689f3..d8716b821f 100644 --- a/app/models/spree/product_set.rb +++ b/app/models/spree/product_set.rb @@ -36,18 +36,17 @@ class Spree::ProductSet < ModelSet def update_product_only_attributes(product, attributes) variant_related_attrs = [:id, :variants_attributes, :master_attributes] + product_related_attrs = attributes.except(*variant_related_attrs) - if attributes.except(*variant_related_attrs).present? - product.assign_attributes(attributes.except(*variant_related_attrs)) + return true if product_related_attrs.blank? - product.variants.each do |variant| - validate_presence_of_unit_value(product, variant) - end + product.assign_attributes(product_related_attrs) - product.save if errors.empty? - else - true + product.variants.each do |variant| + validate_presence_of_unit_value(product, variant) end + + product.save if errors.empty? end def validate_presence_of_unit_value(product, variant) @@ -58,19 +57,13 @@ class Spree::ProductSet < ModelSet end def update_product_variants(product, attributes) - if attributes[:variants_attributes] - update_variants_attributes(product, attributes[:variants_attributes]) - else - true - end + return true unless attributes[:variants_attributes] + update_variants_attributes(product, attributes[:variants_attributes]) end def update_product_master(product, attributes) - if attributes[:master_attributes] - update_variant(product, attributes[:master_attributes]) - else - true - end + return true unless attributes[:master_attributes] + update_variant(product, attributes[:master_attributes]) end def update_variants_attributes(product, variants_attributes) From 60d05a941c611954260b5f4fe0c3f4840068b4ea Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 10 Oct 2018 14:03:14 +0200 Subject: [PATCH 188/190] Fix variants with 'weight' and missing unit_value This adds a data migration to fix those cases. It defaults to showing 1 unit of the specified weight. That is, if the user chose Kg, it'll display 1 as unit. Note that migration logs the process in a log/migrate.log file separate from the regular Rails log/production.log file. When you run the migration you'll see something like: Fixing variants missing unit_value... Processing variant 12... Succesfully fixed variant 12 Done! This helps auditing the changes applied and debug any possible failure scenarios. You can delete it once all is ok. --- ...0093850_fix_variants_missing_unit_value.rb | 49 +++++++++++++++++++ db/schema.rb | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20181010093850_fix_variants_missing_unit_value.rb diff --git a/db/migrate/20181010093850_fix_variants_missing_unit_value.rb b/db/migrate/20181010093850_fix_variants_missing_unit_value.rb new file mode 100644 index 0000000000..c4226d0dc2 --- /dev/null +++ b/db/migrate/20181010093850_fix_variants_missing_unit_value.rb @@ -0,0 +1,49 @@ +# Fixes variants whose product.variant_unit is 'weight' and miss a unit_value, +# showing 1 unit of the specified weight. That is, if the user chose Kg, it'll +# display 1 as unit. +class FixVariantsMissingUnitValue < ActiveRecord::Migration + HUMAN_UNIT_VALUE = 1 + + def up + logger.info "Fixing variants missing unit_value...\n" + + variants_missing_unit_value.find_each do |variant| + logger.info "Processing variant #{variant.id}..." + + fix_unit_value(variant) + end + + logger.info "Done!" + end + + def down + end + + private + + def variants_missing_unit_value + Spree::Variant + .joins(:product) + .readonly(false) + .where( + spree_products: { variant_unit: 'weight' }, + spree_variants: { unit_value: nil } + ) + end + + def fix_unit_value(variant) + variant.unit_value = HUMAN_UNIT_VALUE * variant.product.variant_unit_scale + + if variant.save + logger.info "Successfully fixed variant #{variant.id}" + else + logger.info "Failed fixing variant #{variant.id}" + end + + logger.info "" + end + + def logger + @logger ||= Logger.new('log/migrate.log') + end +end diff --git a/db/schema.rb b/db/schema.rb index 91ef541a32..c29fa95b9e 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 => 20180919102548) do +ActiveRecord::Schema.define(:version => 20181010093850) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false From ebb03906cf01003f8bba84744780073e0f223bbb Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Thu, 11 Oct 2018 15:37:38 +0100 Subject: [PATCH 189/190] Increase rubocop line limit to 100 --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0cdd2b8a55..c2e82f708f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -188,7 +188,7 @@ Metrics/CyclomaticComplexity: Max: 6 Metrics/LineLength: - Max: 80 + Max: 100 Metrics/MethodLength: Max: 10 From fcff83592204923d6be0b9efec69b712cbcf6b15 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Fri, 12 Oct 2018 17:06:42 +0100 Subject: [PATCH 190/190] Update results on dropdown select --- app/views/spree/admin/orders/_per_page_controls.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/orders/_per_page_controls.html.haml b/app/views/spree/admin/orders/_per_page_controls.html.haml index 7a0c3fd081..87e6c50dba 100644 --- a/app/views/spree/admin/orders/_per_page_controls.html.haml +++ b/app/views/spree/admin/orders/_per_page_controls.html.haml @@ -1,5 +1,5 @@ .per-page{'ng-show' => '!RequestMonitor.loading && orders.length > 0'} - %input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'ng-model' => 'per_page'} + %input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'ng-model' => 'per_page', 'ng-change' => 'fetchResults()'} %span.per-page-feedback {{ 'spree.admin.orders.index.results_found' | t:{number: pagination.results} }}