From f2c1e0198ef4d39a1d6523042421f9df3fcf38c4 Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 25 Oct 2024 14:17:42 +0300 Subject: [PATCH 01/25] 920 Added seeds for categories --- db/seeds.rb | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/db/seeds.rb b/db/seeds.rb index 99495cd83..e3d78a3de 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -30,3 +30,47 @@ end FactoryBot.create(:product, :diaper) unless Product.exists?(title: "diaper") + +# Categories +def create_category_with_periods(en_name, uk_name, periods) + unless Category.exists?(en_name: en_name) + category = Category.create!(en_name: en_name, uk_name: uk_name) + + periods.each do |period_data| + DiapersPeriod.create!(period_data.merge(category: category)) + end + end +end + +# Budgetary category periods +create_category_with_periods("Budgetary", "Бюджетна", [ + { period_start: 1, period_end: 3, usage_amount: 10, price: 5.96 }, + { period_start: 4, period_end: 6, usage_amount: 8, price: 6.35 }, + { period_start: 7, period_end: 9, usage_amount: 6, price: 6.35 }, + { period_start: 10, period_end: 12, usage_amount: 6, price: 6.45 }, + { period_start: 13, period_end: 18, usage_amount: 4, price: 6.45 }, + { period_start: 19, period_end: 24, usage_amount: 4, price: 7.6 }, + { period_start: 25, period_end: 30, usage_amount: 2, price: 7.6 } +]) + +# Medium category periods +create_category_with_periods("Medium", "Середня", [ + { period_start: 1, period_end: 3, usage_amount: 10, price: 8.03 }, + { period_start: 4, period_end: 6, usage_amount: 8, price: 8.33 }, + { period_start: 7, period_end: 9, usage_amount: 6, price: 8.65 }, + { period_start: 10, period_end: 12, usage_amount: 6, price: 10.07 }, + { period_start: 13, period_end: 18, usage_amount: 4, price: 10.07 }, + { period_start: 19, period_end: 24, usage_amount: 4, price: 11.13 }, + { period_start: 25, period_end: 30, usage_amount: 2, price: 11.13 } +]) + +# Premium category periods +create_category_with_periods("Premium", "Преміум", [ + { period_start: 1, period_end: 3, usage_amount: 10, price: 11.41 }, + { period_start: 4, period_end: 6, usage_amount: 8, price: 12.59 }, + { period_start: 7, period_end: 9, usage_amount: 6, price: 14.18 }, + { period_start: 10, period_end: 12, usage_amount: 6, price: 18.52 }, + { period_start: 13, period_end: 18, usage_amount: 4, price: 18.52 }, + { period_start: 19, period_end: 24, usage_amount: 4, price: 18.62 }, + { period_start: 25, period_end: 30, usage_amount: 2, price: 18.62 } +]) From f471776f2738f41cf74aa95739b365e37c0f0f3f Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 1 Nov 2024 18:00:46 +0200 Subject: [PATCH 02/25] Restrict input to two decimal places in price input fields --- app/javascript/controllers/price_form_controller.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/javascript/controllers/price_form_controller.js b/app/javascript/controllers/price_form_controller.js index 9ecdf8479..8e2262e64 100644 --- a/app/javascript/controllers/price_form_controller.js +++ b/app/javascript/controllers/price_form_controller.js @@ -13,6 +13,7 @@ export default class extends Controller { this.priceInputTargets.forEach(input => { input.addEventListener('input', this.validatePriceInput.bind(this)); + input.addEventListener('keydown', this.restrictDecimalInput.bind(this)); }); } @@ -57,4 +58,13 @@ export default class extends Controller { target.style.borderColor = ""; } } + + restrictDecimalInput(event) { + const target = event.target; + const inputValue = target.value; + + if (inputValue.includes('.') && inputValue.split('.')[1].length >= 2 && !["Backspace", "Delete"].includes(event.key)) { + event.preventDefault(); + } + } } From 0a0654ef7edbf3ab4d5d19ea0fa1a6526425466f Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 8 Nov 2024 00:44:04 +0200 Subject: [PATCH 03/25] creates show page for calculators --- .../stylesheets/application.tailwind.css | 1 + .../components/showpage_calculator.css | 17 +++++++++++ .../account/calculators_controller.rb | 2 +- app/views/account/calculators/show.html.erb | 29 +++++++++++++++++++ config/locales/en/en.yml | 6 ++++ config/locales/uk/uk.yml | 6 ++++ 6 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 app/assets/stylesheets/components/showpage_calculator.css create mode 100644 app/views/account/calculators/show.html.erb diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index f73f37afc..9a85acf0b 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -10,5 +10,6 @@ @import "/components/pagination"; @import "/components/breadcrumbs.scss"; @import "/components/description_block"; +@import "/components/showpage_calculator"; @import "/utilities/custom-utilities"; @import "/pages/under_construction" diff --git a/app/assets/stylesheets/components/showpage_calculator.css b/app/assets/stylesheets/components/showpage_calculator.css new file mode 100644 index 000000000..700449958 --- /dev/null +++ b/app/assets/stylesheets/components/showpage_calculator.css @@ -0,0 +1,17 @@ +@layer base { + .main-show-container { + @apply flex flex-col bg-white rounded-lg shadow-md w-full p-6 text-left mt-4; + } + .back-arrow { + @apply rounded mb-4 px-2 flex items-center; + } + .calc-details { + @apply flex flex-col mb-4 px-2; + } + .showpage-buttons { + @apply flex justify-start w-full space-x-4; + } + .showpage-text { + @apply text-gray-900; + } + } \ No newline at end of file diff --git a/app/controllers/account/calculators_controller.rb b/app/controllers/account/calculators_controller.rb index 0c85b334e..d0156447b 100644 --- a/app/controllers/account/calculators_controller.rb +++ b/app/controllers/account/calculators_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Account::CalculatorsController < Account::BaseController - before_action :calculator, only: [:edit, :update, :destroy] + before_action :calculator, only: [:show, :edit, :update, :destroy] load_and_authorize_resource def index diff --git a/app/views/account/calculators/show.html.erb b/app/views/account/calculators/show.html.erb new file mode 100644 index 000000000..450da2e0d --- /dev/null +++ b/app/views/account/calculators/show.html.erb @@ -0,0 +1,29 @@ +
+ <%= link_to account_calculators_path, class: "back-arrow" do %> + <%= inline_svg "icons/arrow-left.svg", class: "z-1 mr-2 mb-0.5" %> + <%= t('buttons.back') %> + <% end %> + +
+ <%= t('.name') %>: +

<%= @calculator.name %>

+
+ +
+ <%= t('.slug') %>: +

<%= @calculator.slug %>

+
+ +
+ <%= link_to t('.edit'), + edit_account_calculator_path(@calculator.slug, locale: I18n.locale), + class: "btn btn-green" %> + + <%= button_to account_calculator_path(@calculator.slug, locale: I18n.locale), + method: :delete, + data: { turbo_confirm: t('.confirm_delete') }, + class: "btn btn-danger" do %> + <%= t('.delete') %> + <% end %> +
+
\ No newline at end of file diff --git a/config/locales/en/en.yml b/config/locales/en/en.yml index 1828133a0..a1950fcbb 100644 --- a/config/locales/en/en.yml +++ b/config/locales/en/en.yml @@ -439,6 +439,12 @@ en: update_calculator_button: "Update calculator" new: create_calculator_button: "Create calculator" + show: + confirm_delete: "Are you sure you want to delete this calculator?" + name: "Name" + slug: "Slug" + edit: "Edit" + delete: "Delete" feature_flags: submit_button: "Save" new_calculator_design: diff --git a/config/locales/uk/uk.yml b/config/locales/uk/uk.yml index 57ecc3936..67f265a04 100644 --- a/config/locales/uk/uk.yml +++ b/config/locales/uk/uk.yml @@ -326,6 +326,12 @@ uk: prohibited_to_update: " перешкоджають оновленню калькулятора" prohibited_to_save: " перешкоджають збереженню калькулятора" error: "помилки" + show: + confirm_delete: "Ви впевнені, що хочете видалити цей калькулятор?" + name: "Назва" + slug: "Шлях" + edit: "Редагувати" + delete: "Видалити" site_settings: edit: meta-title: "Налаштування сайту" From c2a2fc8688d00563bb784b433e382a20d204bd9d Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 8 Nov 2024 00:47:16 +0200 Subject: [PATCH 04/25] updated spaces in files --- app/assets/stylesheets/components/showpage_calculator.css | 3 ++- app/views/account/calculators/show.html.erb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/components/showpage_calculator.css b/app/assets/stylesheets/components/showpage_calculator.css index 700449958..3fa30a41d 100644 --- a/app/assets/stylesheets/components/showpage_calculator.css +++ b/app/assets/stylesheets/components/showpage_calculator.css @@ -14,4 +14,5 @@ .showpage-text { @apply text-gray-900; } - } \ No newline at end of file + } + \ No newline at end of file diff --git a/app/views/account/calculators/show.html.erb b/app/views/account/calculators/show.html.erb index 450da2e0d..ae104c9c5 100644 --- a/app/views/account/calculators/show.html.erb +++ b/app/views/account/calculators/show.html.erb @@ -26,4 +26,4 @@ <%= t('.delete') %> <% end %> - \ No newline at end of file + From b24232416950e8f1d19e3caca44a763456319657 Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 8 Nov 2024 00:48:54 +0200 Subject: [PATCH 05/25] updated spaces in files --- app/assets/stylesheets/components/showpage_calculator.css | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/components/showpage_calculator.css b/app/assets/stylesheets/components/showpage_calculator.css index 3fa30a41d..8e3e79c3a 100644 --- a/app/assets/stylesheets/components/showpage_calculator.css +++ b/app/assets/stylesheets/components/showpage_calculator.css @@ -15,4 +15,3 @@ @apply text-gray-900; } } - \ No newline at end of file From 69409c7d72a4480b5da3390e47effe5c34c128ca Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 8 Nov 2024 01:16:19 +0200 Subject: [PATCH 06/25] updated css file --- .../components/showpage_calculator.css | 36 ++++++++++--------- app/views/account/calculators/show.html.erb | 4 +-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/assets/stylesheets/components/showpage_calculator.css b/app/assets/stylesheets/components/showpage_calculator.css index 8e3e79c3a..c106a4e6e 100644 --- a/app/assets/stylesheets/components/showpage_calculator.css +++ b/app/assets/stylesheets/components/showpage_calculator.css @@ -1,17 +1,21 @@ -@layer base { - .main-show-container { - @apply flex flex-col bg-white rounded-lg shadow-md w-full p-6 text-left mt-4; - } - .back-arrow { - @apply rounded mb-4 px-2 flex items-center; - } - .calc-details { - @apply flex flex-col mb-4 px-2; - } - .showpage-buttons { - @apply flex justify-start w-full space-x-4; - } - .showpage-text { - @apply text-gray-900; - } +@layer components { + .main-show-container { + @apply flex flex-col bg-white rounded-lg shadow-md w-full p-6 text-left mt-4; } + + .back-arrow { + @apply rounded mb-4 px-2 flex items-center; + } + + .calc-details { + @apply flex flex-col mb-4 px-2; + } + + .showpage-buttons { + @apply flex justify-start w-full space-x-4; + } + + .showpage-text { + @apply text-slate-600 text-sm; + } +} diff --git a/app/views/account/calculators/show.html.erb b/app/views/account/calculators/show.html.erb index ae104c9c5..9a6fdaaa9 100644 --- a/app/views/account/calculators/show.html.erb +++ b/app/views/account/calculators/show.html.erb @@ -5,12 +5,12 @@ <% end %>
- <%= t('.name') %>: + <%= t('.name') %>:

<%= @calculator.name %>

- <%= t('.slug') %>: + <%= t('.slug') %>:

<%= @calculator.slug %>

From 7ffda2b4809ba162dabd0756c62888c8dca7d55b Mon Sep 17 00:00:00 2001 From: OlexanderVanzuriak <48361492+olexandervanzuriak@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:36:49 +0200 Subject: [PATCH 07/25] 906 Fix scroll bug (#919) * Fix scroll bug * make scrolling smooth --- app/javascript/controllers/results_controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/controllers/results_controller.js b/app/javascript/controllers/results_controller.js index 2fc4578ed..f66dbcc75 100644 --- a/app/javascript/controllers/results_controller.js +++ b/app/javascript/controllers/results_controller.js @@ -20,5 +20,7 @@ export default class extends Controller { this.willBuyDiapersPluralizeTarget.innerHTML = result.to_be_diapers_amount_pluralize; this.boughtDiapersPluralizeTarget.innerHTML = result.used_diapers_amount_pluralize; + + this.element.scrollIntoView({ behavior: "smooth" }); } } From 150cb68d9d60872bced6fbc46e07041a42a70e7b Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 8 Nov 2024 13:55:51 +0200 Subject: [PATCH 08/25] updated yml file and seeds --- db/data/categories.yml | 36 +++++++++++++++++++++++++++++ db/seeds.rb | 51 ++++++++++-------------------------------- 2 files changed, 48 insertions(+), 39 deletions(-) create mode 100644 db/data/categories.yml diff --git a/db/data/categories.yml b/db/data/categories.yml new file mode 100644 index 000000000..8c845fa64 --- /dev/null +++ b/db/data/categories.yml @@ -0,0 +1,36 @@ +categories: + - en_name: Budgetary + uk_name: Бюджетна + priority: 1 + periods: + - {period_start: 1, period_end: 3, usage_amount: 10, price: 5.96} + - {period_start: 4, period_end: 6, usage_amount: 8, price: 6.35} + - {period_start: 7, period_end: 9, usage_amount: 6, price: 6.35} + - {period_start: 10, period_end: 12, usage_amount: 6, price: 6.45} + - {period_start: 13, period_end: 18, usage_amount: 4, price: 6.45} + - {period_start: 19, period_end: 24, usage_amount: 4, price: 7.6} + - {period_start: 25, period_end: 30, usage_amount: 2, price: 7.6} + + - en_name: Medium + uk_name: Середня + priority: 2 + periods: + - {period_start: 1, period_end: 3, usage_amount: 10, price: 8.03} + - {period_start: 4, period_end: 6, usage_amount: 8, price: 8.33} + - {period_start: 7, period_end: 9, usage_amount: 6, price: 8.65} + - {period_start: 10, period_end: 12, usage_amount: 6, price: 10.07} + - {period_start: 13, period_end: 18, usage_amount: 4, price: 10.07} + - {period_start: 19, period_end: 24, usage_amount: 4, price: 11.13} + - {period_start: 25, period_end: 30, usage_amount: 2, price: 11.13} + + - en_name: Premium + uk_name: Преміум + priority: 3 + periods: + - {period_start: 1, period_end: 3, usage_amount: 10, price: 11.41} + - {period_start: 4, period_end: 6, usage_amount: 8, price: 12.59} + - {period_start: 7, period_end: 9, usage_amount: 6, price: 14.18} + - {period_start: 10, period_end: 12, usage_amount: 6, price: 18.52} + - {period_start: 13, period_end: 18, usage_amount: 4, price: 18.52} + - {period_start: 19, period_end: 24, usage_amount: 4, price: 18.62} + - {period_start: 25, period_end: 30, usage_amount: 2, price: 18.62} \ No newline at end of file diff --git a/db/seeds.rb b/db/seeds.rb index e3d78a3de..4719c18c8 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -32,45 +32,18 @@ FactoryBot.create(:product, :diaper) unless Product.exists?(title: "diaper") # Categories -def create_category_with_periods(en_name, uk_name, periods) - unless Category.exists?(en_name: en_name) - category = Category.create!(en_name: en_name, uk_name: uk_name) +categories_data = YAML.load_file(Rails.root.join("db", "data", "categories.yml")) - periods.each do |period_data| - DiapersPeriod.create!(period_data.merge(category: category)) - end - end -end - -# Budgetary category periods -create_category_with_periods("Budgetary", "Бюджетна", [ - { period_start: 1, period_end: 3, usage_amount: 10, price: 5.96 }, - { period_start: 4, period_end: 6, usage_amount: 8, price: 6.35 }, - { period_start: 7, period_end: 9, usage_amount: 6, price: 6.35 }, - { period_start: 10, period_end: 12, usage_amount: 6, price: 6.45 }, - { period_start: 13, period_end: 18, usage_amount: 4, price: 6.45 }, - { period_start: 19, period_end: 24, usage_amount: 4, price: 7.6 }, - { period_start: 25, period_end: 30, usage_amount: 2, price: 7.6 } -]) +def create_category_with_periods(*args) + en_name, uk_name, priority, periods = args + return if Category.exists?(en_name: en_name) -# Medium category periods -create_category_with_periods("Medium", "Середня", [ - { period_start: 1, period_end: 3, usage_amount: 10, price: 8.03 }, - { period_start: 4, period_end: 6, usage_amount: 8, price: 8.33 }, - { period_start: 7, period_end: 9, usage_amount: 6, price: 8.65 }, - { period_start: 10, period_end: 12, usage_amount: 6, price: 10.07 }, - { period_start: 13, period_end: 18, usage_amount: 4, price: 10.07 }, - { period_start: 19, period_end: 24, usage_amount: 4, price: 11.13 }, - { period_start: 25, period_end: 30, usage_amount: 2, price: 11.13 } -]) + category = Category.create!(en_name:, uk_name:, priority:) + periods.each { |period| DiapersPeriod.create!(period.merge(category:)) } +end -# Premium category periods -create_category_with_periods("Premium", "Преміум", [ - { period_start: 1, period_end: 3, usage_amount: 10, price: 11.41 }, - { period_start: 4, period_end: 6, usage_amount: 8, price: 12.59 }, - { period_start: 7, period_end: 9, usage_amount: 6, price: 14.18 }, - { period_start: 10, period_end: 12, usage_amount: 6, price: 18.52 }, - { period_start: 13, period_end: 18, usage_amount: 4, price: 18.52 }, - { period_start: 19, period_end: 24, usage_amount: 4, price: 18.62 }, - { period_start: 25, period_end: 30, usage_amount: 2, price: 18.62 } -]) +categories_data["categories"].each do |category_data| + create_category_with_periods( + *category_data.slice("en_name", "uk_name", "priority", "periods").values + ) +end From 02f38d7a12686bd50cdeaf2a2230bd6ca1fd54eb Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 8 Nov 2024 13:57:43 +0200 Subject: [PATCH 09/25] updates spaces in yaml file --- db/data/categories.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/data/categories.yml b/db/data/categories.yml index 8c845fa64..d392be94c 100644 --- a/db/data/categories.yml +++ b/db/data/categories.yml @@ -33,4 +33,4 @@ categories: - {period_start: 10, period_end: 12, usage_amount: 6, price: 18.52} - {period_start: 13, period_end: 18, usage_amount: 4, price: 18.52} - {period_start: 19, period_end: 24, usage_amount: 4, price: 18.62} - - {period_start: 25, period_end: 30, usage_amount: 2, price: 18.62} \ No newline at end of file + - {period_start: 25, period_end: 30, usage_amount: 2, price: 18.62} From aec0e74972701465209bca2e460b4fc5f5326c60 Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 8 Nov 2024 14:36:39 +0200 Subject: [PATCH 10/25] updated seeds --- db/seeds.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 4719c18c8..5c502c8c7 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -33,13 +33,12 @@ # Categories categories_data = YAML.load_file(Rails.root.join("db", "data", "categories.yml")) - def create_category_with_periods(*args) en_name, uk_name, priority, periods = args return if Category.exists?(en_name: en_name) - category = Category.create!(en_name:, uk_name:, priority:) - periods.each { |period| DiapersPeriod.create!(period.merge(category:)) } + category = Category.create!(en_name: en_name, uk_name: uk_name, priority: priority) + periods.each { |period| DiapersPeriod.create!(period.merge(category: category)) } end categories_data["categories"].each do |category_data| From 9d84572f18684e6c87387c46f5fd88be6d2f06a4 Mon Sep 17 00:00:00 2001 From: DanielVajnagi <82052651+DanielVajnagi@users.noreply.github.com> Date: Sat, 9 Nov 2024 12:31:59 +0200 Subject: [PATCH 11/25] 931 fix selflock bug (#937) * closes #931-fix-selflock-bug * added validation in controller and test * added request test * before_action added * Update app/controllers/account/users_controller.rb Co-authored-by: Ivan Marynych <49816584+loqimean@users.noreply.github.com> * guard style return * Update app/controllers/account/users_controller.rb Co-authored-by: Ivan Marynych <49816584+loqimean@users.noreply.github.com> * Update users_controller.rb * dig * Update users_controller.rb * Update users_controller.rb * Update users_controller.rb --------- Co-authored-by: Ivan Marynych <49816584+loqimean@users.noreply.github.com> --- app/controllers/account/users_controller.rb | 11 +++++++++ app/views/account/users/index.html.erb | 10 +++++--- config/locales/en/en.yml | 1 + config/locales/uk/uk.yml | 1 + spec/features/account/users_spec.rb | 13 ++++++++++ spec/requests/account/users_spec.rb | 27 +++++++++++++++++++++ 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/app/controllers/account/users_controller.rb b/app/controllers/account/users_controller.rb index e36afe64d..4fc589e74 100644 --- a/app/controllers/account/users_controller.rb +++ b/app/controllers/account/users_controller.rb @@ -6,6 +6,7 @@ class Account::UsersController < Account::BaseController layout "account" before_action :set_paper_trail_whodunnit + before_action :blocking_admin, only: :update load_and_authorize_resource @@ -71,6 +72,16 @@ def user_params prms end + def blocking_admin + @user = resource + + return if params.dig(:user, :blocked).blank? || !@user.admin? + + flash[:alert] = t("errors.messages.blocked_user_cannot_be_admin") + + redirect_to account_users_path + end + def collection User.ordered_by_email end diff --git a/app/views/account/users/index.html.erb b/app/views/account/users/index.html.erb index 588018bc4..77d2448f3 100644 --- a/app/views/account/users/index.html.erb +++ b/app/views/account/users/index.html.erb @@ -38,10 +38,12 @@ <%= link_to icon("fa-solid", "eye"), account_user_path(id: user.id) %> <%= link_to icon("fa-solid", "edit"), edit_account_user_path(id: user.id) %> - <%= button_to account_user_path(id: user, user: toggle_block_param(user)), - method: :patch, - data: { turbo_confirm: toggle_confirm(user) } do %> - <%= icon("fa-solid", toggle_class(user)) %> + <% unless user.admin? %> + <%= button_to account_user_path(id: user, user: toggle_block_param(user)), + method: :patch, + data: { turbo_confirm: toggle_confirm(user) } do %> + <%= icon("fa-solid", toggle_class(user)) %> + <% end %> <% end %> diff --git a/config/locales/en/en.yml b/config/locales/en/en.yml index a1950fcbb..00f2143e6 100644 --- a/config/locales/en/en.yml +++ b/config/locales/en/en.yml @@ -822,6 +822,7 @@ en: messages: accepted: "must be accepted" blank: "can't be blank" + blocked_user_cannot_be_admin: "Admin cannot be blocked" confirmation: "doesn't match %{attribute}" empty: "can't be empty" equal_to: "must be equal to %{count}" diff --git a/config/locales/uk/uk.yml b/config/locales/uk/uk.yml index 67f265a04..c77646f31 100644 --- a/config/locales/uk/uk.yml +++ b/config/locales/uk/uk.yml @@ -846,6 +846,7 @@ uk: messages: accepted: "може бути прийнятим" blank: "не може бути порожнім" + blocked_user_cannot_be_admin: "Неможливо заблокувати адміна" confirmation: "не збігається з підтвердженням" empty: "не може бути порожнім" equal_to: "мусить дорівнювати %{count}" diff --git a/spec/features/account/users_spec.rb b/spec/features/account/users_spec.rb index 4ba06de8a..36ce41d8f 100644 --- a/spec/features/account/users_spec.rb +++ b/spec/features/account/users_spec.rb @@ -8,6 +8,7 @@ create(:user, email: "test1@gmail.com", password: "12345878", last_sign_in_at: time_login) end + let!(:admin_user) { create(:user, role: :admin) } include_context :authorize_admin @@ -63,6 +64,7 @@ end accept_confirm { "Are you sure you want to block this user?" } + sleep 3 expect(page).to have_current_path(account_user_path(id: another_user.id)) expect(page).to have_content "Blocked" end @@ -79,11 +81,22 @@ end accept_confirm { "Are you sure you want to unblock this user?" } + sleep 3 expect(page).to have_current_path(account_user_path(id: another_user.id)) expect(page).to have_content "Unblocked" end end + context "when trying to block an admin user" do + it "shows an alert message and redirects to account users path" do + visit account_users_path + + within(:css, "#user-info-#{admin_user.id}") do + expect(page).not_to have_selector("svg.fa-lock-open") # Expect the lock-open button not to be present + end + end + end + context "when edit user`s info correctly" do it "redirects to user info page" do visit edit_account_user_path(id: another_user.id) diff --git a/spec/requests/account/users_spec.rb b/spec/requests/account/users_spec.rb index 2bda4e4e8..05f2afa50 100644 --- a/spec/requests/account/users_spec.rb +++ b/spec/requests/account/users_spec.rb @@ -6,6 +6,7 @@ include_context :authorize_admin let!(:user) { create(:user, last_sign_in_at: Time.current) } + let!(:admin_user) { create(:user, role: :admin) } describe "GET #index" do let(:csv_content) do @@ -117,6 +118,32 @@ end end + describe "PATCH /account/users/:id" do + context "when trying to block an admin user" do + it "sets an alert message and redirects to the admin user account page" do + patch account_user_path(admin_user), params: { user: { blocked: true }} + + expect(response).to redirect_to(account_users_path) + + follow_redirect! + + expect(flash[:alert]).to eq I18n.t("errors.messages.blocked_user_cannot_be_admin") + expect(response.body).to include(I18n.t("errors.messages.blocked_user_cannot_be_admin")) + end + end + + context "when trying to block a non-admin user" do + it "blocks the user successfully" do + patch account_user_path(user), params: { user: { blocked: true }} + + expect(response).to redirect_to(account_user_path(user)) + + follow_redirect! + expect(user.reload.blocked).to be_truthy + end + end + end + describe "DELETE #destroy" do it "destroys the user and redirects to index page" do expect do From 5b84f4da7de2b2307c912d0345a12b64b6218305 Mon Sep 17 00:00:00 2001 From: alexy78 Date: Thu, 14 Nov 2024 19:19:08 +0200 Subject: [PATCH 12/25] updated yml file --- db/data/categories.yml | 105 ++++++++++++++++++++++++++++++++--------- db/seeds.rb | 16 +++---- 2 files changed, 90 insertions(+), 31 deletions(-) diff --git a/db/data/categories.yml b/db/data/categories.yml index d392be94c..c1f24f8ff 100644 --- a/db/data/categories.yml +++ b/db/data/categories.yml @@ -3,34 +3,97 @@ categories: uk_name: Бюджетна priority: 1 periods: - - {period_start: 1, period_end: 3, usage_amount: 10, price: 5.96} - - {period_start: 4, period_end: 6, usage_amount: 8, price: 6.35} - - {period_start: 7, period_end: 9, usage_amount: 6, price: 6.35} - - {period_start: 10, period_end: 12, usage_amount: 6, price: 6.45} - - {period_start: 13, period_end: 18, usage_amount: 4, price: 6.45} - - {period_start: 19, period_end: 24, usage_amount: 4, price: 7.6} - - {period_start: 25, period_end: 30, usage_amount: 2, price: 7.6} + - period_start: 1 + period_end: 3 + usage_amount: 10 + price: 5.96 + - period_start: 4 + period_end: 6 + usage_amount: 8 + price: 6.35 + - period_start: 7 + period_end: 9 + usage_amount: 6 + price: 6.35 + - period_start: 10 + period_end: 12 + usage_amount: 6 + price: 6.45 + - period_start: 13 + period_end: 18 + usage_amount: 4 + price: 6.45 + - period_start: 19 + period_end: 24 + usage_amount: 4 + price: 7.6 + - period_start: 25 + period_end: 30 + usage_amount: 2 + price: 7.6 - en_name: Medium uk_name: Середня priority: 2 periods: - - {period_start: 1, period_end: 3, usage_amount: 10, price: 8.03} - - {period_start: 4, period_end: 6, usage_amount: 8, price: 8.33} - - {period_start: 7, period_end: 9, usage_amount: 6, price: 8.65} - - {period_start: 10, period_end: 12, usage_amount: 6, price: 10.07} - - {period_start: 13, period_end: 18, usage_amount: 4, price: 10.07} - - {period_start: 19, period_end: 24, usage_amount: 4, price: 11.13} - - {period_start: 25, period_end: 30, usage_amount: 2, price: 11.13} + - period_start: 1 + period_end: 3 + usage_amount: 10 + price: 8.03 + - period_start: 4 + period_end: 6 + usage_amount: 8 + price: 8.33 + - period_start: 7 + period_end: 9 + usage_amount: 6 + price: 8.65 + - period_start: 10 + period_end: 12 + usage_amount: 6 + price: 10.07 + - period_start: 13 + period_end: 18 + usage_amount: 4 + price: 10.07 + - period_start: 19 + period_end: 24 + usage_amount: 4 + price: 11.13 + - period_start: 25 + period_end: 30 + usage_amount: 2 + price: 11.13 - en_name: Premium uk_name: Преміум priority: 3 periods: - - {period_start: 1, period_end: 3, usage_amount: 10, price: 11.41} - - {period_start: 4, period_end: 6, usage_amount: 8, price: 12.59} - - {period_start: 7, period_end: 9, usage_amount: 6, price: 14.18} - - {period_start: 10, period_end: 12, usage_amount: 6, price: 18.52} - - {period_start: 13, period_end: 18, usage_amount: 4, price: 18.52} - - {period_start: 19, period_end: 24, usage_amount: 4, price: 18.62} - - {period_start: 25, period_end: 30, usage_amount: 2, price: 18.62} + - period_start: 1 + period_end: 3 + usage_amount: 10 + price: 11.41 + - period_start: 4 + period_end: 6 + usage_amount: 8 + price: 12.59 + - period_start: 7 + period_end: 9 + usage_amount: 6 + price: 14.18 + - period_start: 10 + period_end: 12 + usage_amount: 6 + price: 18.52 + - period_start: 13 + period_end: 18 + usage_amount: 4 + price: 18.52 + - period_start: 19 + period_end: 24 + usage_amount: 4 + price: 18.62 + - period_start: 25 + period_end: 30 + usage_amount: 2 + price: 18.62 \ No newline at end of file diff --git a/db/seeds.rb b/db/seeds.rb index 5c502c8c7..39c27156f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -31,18 +31,14 @@ FactoryBot.create(:product, :diaper) unless Product.exists?(title: "diaper") -# Categories categories_data = YAML.load_file(Rails.root.join("db", "data", "categories.yml")) -def create_category_with_periods(*args) - en_name, uk_name, priority, periods = args + +categories_data["categories"].each do |category_data| return if Category.exists?(en_name: en_name) - category = Category.create!(en_name: en_name, uk_name: uk_name, priority: priority) - periods.each { |period| DiapersPeriod.create!(period.merge(category: category)) } -end + category = Category.create!(category_data.slice("en_name", "uk_name", "priority")) -categories_data["categories"].each do |category_data| - create_category_with_periods( - *category_data.slice("en_name", "uk_name", "priority", "periods").values - ) + period_records = category_data["periods"].map { |period_data| period_data.merge("category_id" => category.id) } + + DiapersPeriod.insert_all(period_records) end From 03b2931074c08aae3758fcbe27ff48855f4c2044 Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 15 Nov 2024 00:00:53 +0200 Subject: [PATCH 13/25] changed seeds for categories --- db/seeds.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 39c27156f..10c85e359 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -31,14 +31,15 @@ FactoryBot.create(:product, :diaper) unless Product.exists?(title: "diaper") +# Categories categories_data = YAML.load_file(Rails.root.join("db", "data", "categories.yml")) categories_data["categories"].each do |category_data| - return if Category.exists?(en_name: en_name) + next if Category.exists?(en_name: category_data["en_name"]) - category = Category.create!(category_data.slice("en_name", "uk_name", "priority")) + periods = category_data.delete("periods") + category = Category.create!(**category_data) - period_records = category_data["periods"].map { |period_data| period_data.merge("category_id" => category.id) } - - DiapersPeriod.insert_all(period_records) + periods.map! { |period_data| period_data.merge(category_id: category.id) } + periods.each { |period_data| DiapersPeriod.create!(**period_data) } end From c135b4730f7586bf87b24b39311daab741283b52 Mon Sep 17 00:00:00 2001 From: alexy78 Date: Fri, 15 Nov 2024 00:05:12 +0200 Subject: [PATCH 14/25] added extra space to yml file --- db/data/categories.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/data/categories.yml b/db/data/categories.yml index c1f24f8ff..c939cf1b1 100644 --- a/db/data/categories.yml +++ b/db/data/categories.yml @@ -96,4 +96,4 @@ categories: - period_start: 25 period_end: 30 usage_amount: 2 - price: 18.62 \ No newline at end of file + price: 18.62 From e974077de2007cd3139f0f1e8a5f2513997b7fa8 Mon Sep 17 00:00:00 2001 From: SleekMutt <84468904+SleekMutt@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:01:42 +0200 Subject: [PATCH 15/25] 910 Created menstrual hygiene products calculator (#924) * Created menstrual hygiene calculator. Created corresponding to it view, api controller, service and validator. Also added new corresponding action to render mhc calculator to calculators controller. * Fixed link and rubocop offences * Updated rubocop target version and extracted methods in pad usage service * Deleted redundant skip * Added tests for api controller * Fixed some files * Removed redundant code from controllers action * Added smooth scroll * Fixed localization with links * Fixed rubocops target version * Fixed typo * Fixed CI * Refactored code for a better style * Added tests for validator * Fixed offenses * Added tests for each of the validate param method * Retrieved shared example to separate file * Added before hook for blank param --------- Co-authored-by: Ivan Marynych <49816584+loqimean@users.noreply.github.com> Co-authored-by: loqimean --- .rubocop.yml | 2 +- app/assets/images/pad_scales.png | Bin 0 -> 23169 bytes app/assets/images/pads_bought.png | Bin 0 -> 4761 bytes app/assets/images/pads_to_buy.png | Bin 0 -> 5005 bytes .../api/v1/pad_calculators_controller.rb | 20 ++++ app/controllers/calculators_controller.rb | 5 + app/helpers/calculators_helper.rb | 9 ++ .../controllers/mhc_calculator_controller.js | 71 +++++++++++++ .../controllers/pad_results_controller.js | 21 ++++ app/services/calculators/pad_usage_service.rb | 52 ++++++++++ app/validators/mhc_calculator_validator.rb | 55 +++++++++++ app/views/calculators/mhc_calculator.erb | 93 ++++++++++++++++++ .../layouts/_mhc_description_block.html.erb | 14 +++ config/locales/en/en.yml | 43 ++++++-- config/locales/uk/uk.yml | 43 ++++++-- config/routes.rb | 3 + spec/features/account/users_spec.rb | 2 +- spec/requests/calculators_spec.rb | 10 ++ spec/requests/pad_calculators_spec.rb | 45 +++++++++ .../calculators/pad_usage_service_spec.rb | 29 ++++++ .../shared/presence_validation_example.rb | 17 ++++ .../mhc_calculator_validator_spec.rb | 65 ++++++++++++ 22 files changed, 577 insertions(+), 22 deletions(-) create mode 100644 app/assets/images/pad_scales.png create mode 100644 app/assets/images/pads_bought.png create mode 100644 app/assets/images/pads_to_buy.png create mode 100644 app/controllers/api/v1/pad_calculators_controller.rb create mode 100644 app/javascript/controllers/mhc_calculator_controller.js create mode 100644 app/javascript/controllers/pad_results_controller.js create mode 100644 app/services/calculators/pad_usage_service.rb create mode 100644 app/validators/mhc_calculator_validator.rb create mode 100644 app/views/calculators/mhc_calculator.erb create mode 100644 app/views/layouts/_mhc_description_block.html.erb create mode 100644 spec/requests/pad_calculators_spec.rb create mode 100644 spec/services/calculators/pad_usage_service_spec.rb create mode 100644 spec/support/shared/presence_validation_example.rb create mode 100644 spec/validators/mhc_calculator_validator_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 51dec8a54..0d435ded4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -18,7 +18,7 @@ inherit_gem: AllCops: SuggestExtensions: true NewCops: enable - TargetRubyVersion: 3.0.6 + TargetRubyVersion: 3.2 Layout/SpaceInsideHashLiteralBraces: Enabled: true diff --git a/app/assets/images/pad_scales.png b/app/assets/images/pad_scales.png new file mode 100644 index 0000000000000000000000000000000000000000..bae34308887bc2059324a671555d0653f70ccbfd GIT binary patch literal 23169 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 ayLX>r4wl5D9=bDtpRvA$-g6yS!v6uzY>xu~ literal 0 HcmV?d00001 diff --git a/app/assets/images/pads_bought.png b/app/assets/images/pads_bought.png new file mode 100644 index 0000000000000000000000000000000000000000..7a864b55a8b355eddb94bb6ca41a78100054f907 GIT binary patch literal 4761 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/pads_to_buy.png b/app/assets/images/pads_to_buy.png new file mode 100644 index 0000000000000000000000000000000000000000..0fcf41cda70e8e3a161c4df2ec15f9990196b105 GIT binary patch literal 5005 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 XI}cf { + const targetKey = errorKey.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase()); + const feedbackDiv = document.createElement('div'); + + feedbackDiv.className = 'invalid-feedback'; + feedbackDiv.textContent = errors[errorKey]; + + this[`${targetKey}Target`].classList.add("is-invalid"); + this[`${targetKey}Target`].insertAdjacentElement('afterend', feedbackDiv); + }); + } + + clearErrors(){ + this.constructor.targets.forEach(targetKey => { + const targetElement = this[`${targetKey}Target`]; + targetElement.classList.remove("is-invalid"); + + const feedbackDiv = targetElement.nextElementSibling; + if (feedbackDiv && feedbackDiv.classList.contains('invalid-feedback')) { + feedbackDiv.remove(); + } + }); + } +} diff --git a/app/javascript/controllers/pad_results_controller.js b/app/javascript/controllers/pad_results_controller.js new file mode 100644 index 000000000..a998666df --- /dev/null +++ b/app/javascript/controllers/pad_results_controller.js @@ -0,0 +1,21 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = [ + "padsUsed", + "padsToBeUsed", + "moneySpent", + "moneyWillBeSpent" + ]; + + showResults(data) { + let result = data; + + this.moneySpentTarget.innerHTML = Math.ceil(result.already_used_products_cost); + this.moneyWillBeSpentTarget.innerHTML = Math.ceil(result.products_to_be_used_cost); + this.padsUsedTarget.innerHTML = Math.ceil(result.already_used_products); + this.padsToBeUsedTarget.innerHTML = Math.ceil(result.products_to_be_used); + + this.element.scrollIntoView({ behavior: "smooth" }); + } +} diff --git a/app/services/calculators/pad_usage_service.rb b/app/services/calculators/pad_usage_service.rb new file mode 100644 index 000000000..5de628434 --- /dev/null +++ b/app/services/calculators/pad_usage_service.rb @@ -0,0 +1,52 @@ +class Calculators::PadUsageService + attr_accessor :user_age, :menstruation_age, :menopause_age, + :average_menstruation_cycle_duration, + :pads_per_cycle, :pad_category + + PAD_PRICES = { + budget: 2, + average: 4, + premium: 7 + } + + def initialize(user_age:, menstruation_age:, menopause_age:, average_menstruation_cycle_duration:, + pads_per_cycle:, pad_category:) + @user_age = user_age + @menstruation_age = menstruation_age + @menopause_age = menopause_age || 48.7 + @average_menstruation_cycle_duration = average_menstruation_cycle_duration + @pads_per_cycle = pads_per_cycle + @pad_category = (pad_category || :budget).to_sym + end + + def calculate + { + already_used_products:, + already_used_products_cost:, + products_to_be_used:, + products_to_be_used_cost: + } + end + + private + + def already_used_products + menstruations_from_age_range(menstruation_age, user_age) * pads_per_cycle + end + + def products_to_be_used + menstruations_from_age_range(user_age, menopause_age) * pads_per_cycle + end + + def already_used_products_cost + already_used_products * PAD_PRICES[pad_category] + end + + def products_to_be_used_cost + products_to_be_used * PAD_PRICES[pad_category] + end + + def menstruations_from_age_range(from_age, till_age) + (till_age - from_age) * (365 / average_menstruation_cycle_duration) + end +end diff --git a/app/validators/mhc_calculator_validator.rb b/app/validators/mhc_calculator_validator.rb new file mode 100644 index 000000000..cd5e21bfe --- /dev/null +++ b/app/validators/mhc_calculator_validator.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +class MhcCalculatorValidator + attr_reader :params, :errors + + def initialize(params) + @params = params + @errors = {} + end + + def valid? + validate_user_age + validate_menstruation_age + validate_menopause_age + validate_average_menstruation_cycle_duration + validate_pads_per_cycle + validate_pad_category + + errors.empty? + end + + private + + def validate_user_age + presence_valid?(:user_age) + end + + def validate_menstruation_age + presence_valid?(:menstruation_age) + end + + def validate_menopause_age + presence_valid?(:menopause_age) + end + + def validate_average_menstruation_cycle_duration + presence_valid?(:average_menstruation_cycle_duration) + end + + def validate_pads_per_cycle + presence_valid?(:pads_per_cycle) + end + + def validate_pad_category + presence_valid?(:pad_category) + end + + def presence_valid?(param) + return true if @params[param].present? + + @errors[param] = I18n.t("calculators.errors.presence_error_msg", field: I18n.t("calculators.mhc_calculator.form.#{param}")) + + false + end +end diff --git a/app/views/calculators/mhc_calculator.erb b/app/views/calculators/mhc_calculator.erb new file mode 100644 index 000000000..16cd4d748 --- /dev/null +++ b/app/views/calculators/mhc_calculator.erb @@ -0,0 +1,93 @@ +

+

<%= t(".calculator_name") %>

+
+ <%= simple_form_for(:pad, url: "#", html: { class: "simple_form_calculator", + data: { controller: "mhc-calculator", + mhc_calculator_url_value: api_v1_pad_calculators_url, + mhc_calculator_pad_results_outlet: ".result" } }) do |form| %> +
+ +
+ <%= label_tag "child_product_category", t(".form.user_age") %> +
+ + <%= form.input_field :user_age, + type: "number", + class: "form_fild price_select rounded w-100 form-control mb-0", + data: { mhc_calculator_target: "userAge" } %> + +
+ <%= label_tag "child_product_category", t(".form.menstruation_age") %> +
+ + <%= form.input_field :menstruation_age, + type: "number", + class: "form_fild price_select rounded w-100 form-control mb-0 ", + data: { mhc_calculator_target: "menstruationAge" } %> + +
+ <%= label_tag "child_product_category", t(".form.menopause_age") %> +
+ + <%= form.input_field :menopause_age, + type: "number", + class: "form_fild price_select rounded w-100 form-control mb-0", + data: { mhc_calculator_target: "menopauseAge" } %> + +
+ <%= label_tag "child_product_category", t(".form.average_menstruation_cycle_duration") %> +
+ + <%= form.input_field :average_menstruation_cycle_duration, + type: "number", + class: "form_fild price_select rounded w-100 form-control mb-0", + data: { mhc_calculator_target: "averageMenstruationCycleDuration" } %> + +
+ <%= label_tag "child_product_category", t(".form.pads_per_cycle") %> +
+ + <%= form.input_field :pads_per_cycle, + type: "number", + class: "form_fild price_select rounded w-100 form-control mb-0", + data: { mhc_calculator_target: "padsPerCycle" } %> + +
+ <%= label_tag "child_product_category", t(".form.pad_category") %> +
+ + <%= form.input_field :product_category, + collection: [[t(".form.budgetary"), :budget], [t(".form.average"), :average], [t(".form.premium"), :premium]], + selected: "Budget", + class: "form_fild price_select rounded w-100 form-control mb-0", + data: { mhc_calculator_target: "padCategory" } %> + + <%= form.submit t("calculators.buttons.calculate"), + class: "calculate-btn result-btn mt-6", + data: { action: "mhc-calculator#submit" } %> +
+ <% end %> + + <%= image_tag "pad_scales.png", class: "scales_img", alt: "Scales" %> +
+
+ +
+ <% mhc_calculator_items.each do |item| %> + <% if item == "arrow" %> +
+ <%= image_tag "icons/vector_5.png", class: "vector", alt: "horizontal arrow" %> + <%= image_tag "icons/vector_2.png", class: "vector-mobile", alt: "vertical arrow" %> +
+ <% else %> +
+ <%= image_tag item[:image], class: "img-margin", alt: "icon" %> +

0

+

<%= item[:unit] %>

+

<%= item[:text] %>

+
+ <% end %> + <% end %> +
+ +<%= render "layouts/mhc_description_block" %> diff --git a/app/views/layouts/_mhc_description_block.html.erb b/app/views/layouts/_mhc_description_block.html.erb new file mode 100644 index 000000000..51d89f1b9 --- /dev/null +++ b/app/views/layouts/_mhc_description_block.html.erb @@ -0,0 +1,14 @@ +
+
+
+

+ <%= t(".mhc_description_block_html", + information_link: link_to_external(text: t(".information_link_text"), url: t(".information_link") , class: 'description-link'), + contact_link: link_to_external(text: t(".contact_link_text"), url: t(".contact_link"), class: 'description-link')) %> +

+
+
+ <%= link_to_external(text: t('.use_less'), url: t(".use_less_link"), class: 'action-button btn-green') %> +
+
+
diff --git a/config/locales/en/en.yml b/config/locales/en/en.yml index 00f2143e6..4a624b943 100644 --- a/config/locales/en/en.yml +++ b/config/locales/en/en.yml @@ -52,6 +52,24 @@ en: calculators: index: meta-title: "Calculators" + mhc_calculator: + calculator_name: "Menstrual hygiene products calculator" + form: + user_age: "Your age" + menstruation_age: "Age at First Menstruation" + menopause_age: "Age at Menopause" + average_menstruation_cycle_duration: "Average Menstrual Cycle Duration (days)" + pads_per_cycle: "Average Products Used Per Cycle" + pad_category: "Product Category" + budgetary: "Budgetary" + average: "Average" + premium: "Premium" + bought_products: "already used" + will_buy_products: "to be used in the future" + money_spent: "already spent on menstrual hygiene products" + money_will_be_spent: "yet to be spent on menstrual hygiene products" + pieces: "pcs." + unit: "uah" new_calculator: diaper_сalculator: "Diaper calculator" form: @@ -81,6 +99,7 @@ en: year_and_month_error_msg: "Please, select years and months" year_error_msg: "Please, select years" month_error_msg: "Please, select month" + presence_error_msg: "%{field} is missing" show: welcome_header: "Welcome to ZeroWaste" buttons: @@ -127,6 +146,20 @@ en: method_link: https://zerowastelviv.org.ua/diaper-calculator-method-en/ contact_link: https://zerowastelviv.org.ua/contacts/ use_less_link: https://zerowastelviv.org.ua/en/zero-waste-nappies-en/ + mhc_description_block: + mhc_description_block_html: + "These data are approximate, and exact values may vary depending on your individual needs and habits. + Using reusable menstrual products can significantly reduce waste and serve as a cost-effective alternative to disposable options. + You can optimize the use of these products at any stage, which will help lower expenses and increase comfort. + We recommend exploring methods for the effective use of reusable menstrual products and learning about their benefits. + More %{information_link} about the calculations regarding the quantity and cost of these products can be found in our materials + or by %{contact_link} with your questions." + information_link_text: "detailed information" + contact_link_text: "contacting us" + information_link: "https://docs.google.com/document/d/1xu6cch-Ner9OEpU4_TV5suIUsuVcWpgiUuX6QrOgaL4" + contact_link: "https://zerowastelviv.org.ua/en/contacts/" + use_less: "HOW TO USE LESS" + use_less_link: "https://zerowastelviv.org.ua/en/zero-waste-period/" homepage: homepage_title: "Welcome to the diaper calculator" description: @@ -439,12 +472,6 @@ en: update_calculator_button: "Update calculator" new: create_calculator_button: "Create calculator" - show: - confirm_delete: "Are you sure you want to delete this calculator?" - name: "Name" - slug: "Slug" - edit: "Edit" - delete: "Delete" feature_flags: submit_button: "Save" new_calculator_design: @@ -461,9 +488,6 @@ en: email: "Email" password: "Password" shared: - navigation: - donate: "DONATE" - donate_link: "https://zerowastelviv.org.ua/pidtrymaty/" links: forgot_your_password: "Forgot your password?" log_in: "Log In" @@ -822,7 +846,6 @@ en: messages: accepted: "must be accepted" blank: "can't be blank" - blocked_user_cannot_be_admin: "Admin cannot be blocked" confirmation: "doesn't match %{attribute}" empty: "can't be empty" equal_to: "must be equal to %{count}" diff --git a/config/locales/uk/uk.yml b/config/locales/uk/uk.yml index c77646f31..0b52f53aa 100644 --- a/config/locales/uk/uk.yml +++ b/config/locales/uk/uk.yml @@ -21,6 +21,24 @@ uk: calculators: index: meta-title: "Калькулятори" + mhc_calculator: + calculator_name: "Калькулятор менструальних гігієнічних засобів" + form: + user_age: "Ваш вік" + menstruation_age: "Вік початку менструації" + menopause_age: "Вік настання менопаузи" + average_menstruation_cycle_duration: "Середня тривалість менструального циклу (дні)" + pads_per_cycle: "Середня кількість засобів гігієни за цикл" + pad_category: "Категорія засобів гігієни" + budgetary: "Бюджетна" + average: "Середня" + premium: "Преміум" + bought_products: "ви вже використали" + will_buy_products: "ви ще використаєте" + money_spent: "ви вже витратили" + money_will_be_spent: "ви ще витратите" + pieces: "шт." + unit: "грн" new_calculator: diaper_сalculator: "Калькулятор підгузків" form: @@ -58,6 +76,7 @@ uk: year_and_month_error_msg: "Будь ласка, виберіть, скільки років та місяців дитині" year_error_msg: "Будь ласка, виберіть, скільки років дитині" month_error_msg: "Будь ласка, виберіть, скільки місяців дитині" + presence_error_msg: "Поле \"%{field}\" відсутнє" show: welcome_header: "Ласкаво просимо до ZeroWaste" calculate_result_button: "Pозрахувати" @@ -109,6 +128,20 @@ uk: method_link: "https://zerowastelviv.org.ua/diaper-calculator-method/" contact_link: "https://zerowastelviv.org.ua/kontakty/" use_less_link: "https://zerowastelviv.org.ua/zero-waste-nappies/" + mhc_description_block: + mhc_description_block_html: + "Ці дані є орієнтовними, і точні значення можуть варіюватися залежно від ваших індивідуальних потреб та звичок. + Використання багаторазових менструальних засобів може значно зменшити кількість відходів і стати економічно + вигідною альтернативою одноразовим продуктам. Ви можете оптимізувати використання цих засобів на будь-якому етапі, + що дозволить знизити витрати та підвищити комфорт. Ми рекомендуємо ознайомитися з методами ефективного використання + багаторазових менструальних продуктів та їх перевагами. Більш %{information_link} про розрахунки кількості та вартості + цих продуктів можна знайти в наших матеріалах або звернувшись %{contact_link} з вашими питаннями." + information_link_text: "детальну інформацію" + contact_link_text: "до нас" + information_link: "https://docs.google.com/document/d/1xu6cch-Ner9OEpU4_TV5suIUsuVcWpgiUuX6QrOgaL4" + contact_link: "https://zerowastelviv.org.ua/kontakty/" + use_less: "ЯК ЗМЕНШИТИ ВИКОРИСТАННЯ" + use_less_link: "https://zerowastelviv.org.ua/reuse-zero-waste-menstruatsii/" homepage: homepage_title: "Вас вітає калькулятор підгузків" description: "Цей калькулятор дає змогу порахувати, скільки одноразових підгузків ваша дитина вже використала та ще використає до того часу, як перейде на звичайну білизну. А також визначити вартість уже куплених підгузків та суму, яку ви витратите на них у майбутньому." @@ -326,12 +359,6 @@ uk: prohibited_to_update: " перешкоджають оновленню калькулятора" prohibited_to_save: " перешкоджають збереженню калькулятора" error: "помилки" - show: - confirm_delete: "Ви впевнені, що хочете видалити цей калькулятор?" - name: "Назва" - slug: "Шлях" - edit: "Редагувати" - delete: "Видалити" site_settings: edit: meta-title: "Налаштування сайту" @@ -421,9 +448,6 @@ uk: email: "Електронна пошта" password: "Пароль" shared: - navigation: - donate: "Підтримати" - donate_link: "https://zerowastelviv.org.ua/pidtrymaty/" links: forgot_your_password: "Забули пароль?" log_in: "Увійти" @@ -846,7 +870,6 @@ uk: messages: accepted: "може бути прийнятим" blank: "не може бути порожнім" - blocked_user_cannot_be_admin: "Неможливо заблокувати адміна" confirmation: "не збігається з підтвердженням" empty: "не може бути порожнім" equal_to: "мусить дорівнювати %{count}" diff --git a/config/routes.rb b/config/routes.rb index 7de991fc5..c4ef467b8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,6 +31,7 @@ get "/sitemap", to: "sitemap#index" get "/calculator", to: "calculators#calculator" + get "/mhc_calculator", to: "calculators#mhc_calculator" post "/receive_recomendations", to: "calculators#receive_recomendations" get "about-us", to: "home#about", as: "about" @@ -78,6 +79,8 @@ end post "/diaper_calculators", to: "diaper_calculators#calculate" + post "/pad_calculators", + to: "pad_calculators#calculate" end namespace :v2 do resources :calculators, only: [] do diff --git a/spec/features/account/users_spec.rb b/spec/features/account/users_spec.rb index 36ce41d8f..d9c51f171 100644 --- a/spec/features/account/users_spec.rb +++ b/spec/features/account/users_spec.rb @@ -92,7 +92,7 @@ visit account_users_path within(:css, "#user-info-#{admin_user.id}") do - expect(page).not_to have_selector("svg.fa-lock-open") # Expect the lock-open button not to be present + expect(page).to have_no_css("svg.fa-lock-open") # Expect the lock-open button not to be present end end end diff --git a/spec/requests/calculators_spec.rb b/spec/requests/calculators_spec.rb index dacbea773..2be0577e0 100644 --- a/spec/requests/calculators_spec.rb +++ b/spec/requests/calculators_spec.rb @@ -110,6 +110,16 @@ end end + describe "GET /mhc_calculator" do + it "renders pad calculator" do + get mhc_calculator_path + + expect(response).to be_successful + expect(response).to render_template(:mhc_calculator) + expect(response.body).to include("results") + end + end + describe "POST #create" do include_context :authorize_admin diff --git a/spec/requests/pad_calculators_spec.rb b/spec/requests/pad_calculators_spec.rb new file mode 100644 index 000000000..c2787f9ea --- /dev/null +++ b/spec/requests/pad_calculators_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::V1::PadCalculatorsController, type: :request do + let(:expected_result) do + { + already_used_products: 4420, + already_used_products_cost: 8840, + products_to_be_used: 5460, + products_to_be_used_cost: 10920 + } + end + + let(:valid_params) do + { + user_age: 30, + menstruation_age: 13, + menopause_age: 51, + average_menstruation_cycle_duration: 28, + pads_per_cycle: 20, + pad_category: :budget + } + end + + describe "POST /pad_calculators" do + context "when params are valid" do + it "return expected results" do + post api_v1_pad_calculators_path, params: valid_params, as: :json + + expect(response).to be_successful + expect(response.body).to eq(expected_result.to_json) + end + end + + context "when params are invalid" do + it "return errors" do + post api_v1_pad_calculators_path + + expect(response).to be_unprocessable + expect(JSON.parse(response.body, symbolize_names: true)).to have_key(:errors) + end + end + end +end diff --git a/spec/services/calculators/pad_usage_service_spec.rb b/spec/services/calculators/pad_usage_service_spec.rb new file mode 100644 index 000000000..3948cef44 --- /dev/null +++ b/spec/services/calculators/pad_usage_service_spec.rb @@ -0,0 +1,29 @@ +require "rails_helper" + +RSpec.describe Calculators::PadUsageService do + describe "#calculate" do + let!(:service) do + described_class.new( + user_age: 30, + menstruation_age: 13, + menopause_age: 51, + pads_per_cycle: 20, + average_menstruation_cycle_duration: 28, + pad_category: :budget + ) + end + + before do + service.calculate + end + + let(:result) { service.calculate } + + it "calculates the correct values" do + expect(result[:already_used_products]).to eq(4_420.0) + expect(result[:already_used_products_cost]).to eq(8_840.0) + expect(result[:products_to_be_used]).to eq(5_460.0) + expect(result[:products_to_be_used_cost]).to eq(10_920.0) + end + end +end diff --git a/spec/support/shared/presence_validation_example.rb b/spec/support/shared/presence_validation_example.rb new file mode 100644 index 000000000..7138d9d5d --- /dev/null +++ b/spec/support/shared/presence_validation_example.rb @@ -0,0 +1,17 @@ +shared_examples "presence validation" do |attribute| + it "#{attribute} is valid when present" do + expect(validator.send(:"validate_#{attribute}")).to be true + expect(validator.errors[attribute]).to be_nil + end + + context "#{attribute} is not present" do + before do + validator.params[attribute] = nil + end + + it "#{attribute} is invalid when blank" do + expect(validator.send(:"validate_#{attribute}")).to be false + expect(validator.errors[attribute]).to eq(I18n.t("#{local_error_prefix}.presence_error_msg", field: I18n.t("#{local_field_prefix}.#{attribute}"))) + end + end +end diff --git a/spec/validators/mhc_calculator_validator_spec.rb b/spec/validators/mhc_calculator_validator_spec.rb new file mode 100644 index 000000000..8c4dc67b8 --- /dev/null +++ b/spec/validators/mhc_calculator_validator_spec.rb @@ -0,0 +1,65 @@ +# spec/validators/mhc_calculator_validator_spec.rb + +require "rails_helper" + +RSpec.describe MhcCalculatorValidator do + let(:valid_params) do + { + user_age: 30, + menstruation_age: 13, + menopause_age: 50, + average_menstruation_cycle_duration: 28, + pads_per_cycle: 10, + pad_category: "budget" + } + end + let!(:validator) { described_class.new(valid_params) } + + let(:local_error_prefix) { "calculators.errors" } + let(:local_field_prefix) { "calculators.mhc_calculator.form" } + + describe "#valid?" do + context "when all parameters are valid" do + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors).to be_empty + end + end + + context "when some parameter is missing" do + let(:invalid_params_validator) { described_class.new(valid_params.except(:user_age)) } + + it "returns false" do + expect(invalid_params_validator.valid?).to be false + expect(invalid_params_validator.errors).to include(:user_age) + expect(invalid_params_validator.errors[:user_age]).to eq(I18n.t("#{local_error_prefix}.presence_error_msg", field: I18n.t("#{local_field_prefix}.user_age"))) + end + end + end + + describe "individual validations" do + context "user age validation" do + include_examples "presence validation", :user_age + end + + context "menstruation age validation" do + include_examples "presence validation", :menstruation_age + end + + context "menopause age validation" do + include_examples "presence validation", :menopause_age + end + + context "average menstruation cycle duration validation" do + include_examples "presence validation", :average_menstruation_cycle_duration + end + + context "pads per cycle validation" do + include_examples "presence validation", :pads_per_cycle + end + + context "pad category validation" do + include_examples "presence validation", :pad_category + end + end +end From e3ee22d0194f2b85a53a2eda21ed3a3561645a28 Mon Sep 17 00:00:00 2001 From: Andrii Date: Thu, 14 Nov 2024 12:54:49 +0000 Subject: [PATCH 16/25] add branch --- .github/workflows/ci.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a13be3851..77129c821 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,16 @@ name: CI on: push: + pull_request: + branches: + - develop + - master + types: + - closed + + release: + types: [published] + jobs: rubocop: runs-on: ubuntu-latest @@ -50,14 +60,14 @@ jobs: with: ruby-version: 3.3.5 bundler-cache: true - + - name: Install system dependencies run: sudo apt-get install -y libvips42 libvips-dev imagemagick - uses: actions/setup-node@v1 with: - node-version: '14.x' - registry-url: 'https://registry.npmjs.org' + node-version: "14.x" + registry-url: "https://registry.npmjs.org" - uses: nanasess/setup-chromedriver@master From bb41c570eb352056725b1e0b9fb56102285ba002 Mon Sep 17 00:00:00 2001 From: NVMakarenko Date: Tue, 19 Nov 2024 14:56:50 +0200 Subject: [PATCH 17/25] fix refresh newCategory page --- app/views/account/categories/new.html.erb | 18 +++--------------- .../account/categories/new.turbo_stream.erb | 4 ++++ .../categories/partials/new/_form.html.erb | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 app/views/account/categories/new.turbo_stream.erb create mode 100644 app/views/account/categories/partials/new/_form.html.erb diff --git a/app/views/account/categories/new.html.erb b/app/views/account/categories/new.html.erb index f9f6156d6..1bdd33494 100644 --- a/app/views/account/categories/new.html.erb +++ b/app/views/account/categories/new.html.erb @@ -1,15 +1,3 @@ -<%= simple_form_for @category, url: { action: "create" }, html: { novalidate: false } do |f| %> -
-
- <%= f.input :uk_name, class: "form-control col-sm-11" %> - <%= f.input :en_name, class: "form-control col-sm-11" %> - <%= f.input :priority, collection: Category::PRIORITY_RANGE, wrapper: :custom_vertical_select %> -
-
-
- <%= f.submit t(".form.create_category_button"), class: "btn btn-green me-2" %> - <%= link_to account_categories_path, class: "btn btn-danger d-flex align-items-center justify-content-center" do %> - <%= t("buttons.cancel") %> - <% end %> -
-<% end %> +
+ <%= render "account/categories/partials/new/form", category: @category %> +
diff --git a/app/views/account/categories/new.turbo_stream.erb b/app/views/account/categories/new.turbo_stream.erb new file mode 100644 index 000000000..0de8de8e1 --- /dev/null +++ b/app/views/account/categories/new.turbo_stream.erb @@ -0,0 +1,4 @@ +<%= turbo_stream.replace( + dom_id(@category, :form), + partial: "account/categories/partials/new/form", + locals: {category: @category}) %> diff --git a/app/views/account/categories/partials/new/_form.html.erb b/app/views/account/categories/partials/new/_form.html.erb new file mode 100644 index 000000000..f0ad59de7 --- /dev/null +++ b/app/views/account/categories/partials/new/_form.html.erb @@ -0,0 +1,15 @@ +<%= simple_form_for @category, url: { action: "create" }, html: { id: dom_id(category, :form) } do |f| %> +
+
+ <%= f.input :uk_name, class: "form-control col-sm-11" %> + <%= f.input :en_name, class: "form-control col-sm-11" %> + <%= f.input :priority, collection: Category::PRIORITY_RANGE, wrapper: :custom_vertical_select %> +
+
+
+ <%= f.submit t("account.categories.new.form.create_category_button"), class: "btn btn-green me-2" %> + <%= link_to account_categories_path, class: "btn btn-danger d-flex align-items-center justify-content-center" do %> + <%= t("buttons.cancel") %> + <% end %> +
+<% end %> From f00318645322bf2754ab9d347b5098f6070c948e Mon Sep 17 00:00:00 2001 From: Andrii Date: Tue, 19 Nov 2024 14:04:11 +0000 Subject: [PATCH 18/25] add pipeline --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77129c821..315493d3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,3 +89,39 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} json-path: tmp/rspec_results.json if: always() + + deploy-to-develop: + needs: rspec + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true + steps: + - uses: actions/checkout@v2 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3.5 + bundler-cache: true + + - uses: miloserdow/capistrano-deploy@v3 + with: + target: development + deploy_key: ${{ secrets.DEV_DEPLOY_KEY }} + + deploy-to-production: + needs: rspec + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/checkout@v2 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3.5 + bundler-cache: true + + - uses: miloserdow/capistrano-deploy@v3 + with: + target: production + deploy_key: ${{ secrets.PROD_DEPLOY_KEY }} From 2eedb174740c20b1f021e3457bea25d44f9d68a0 Mon Sep 17 00:00:00 2001 From: Artem Stepanchenko Date: Tue, 19 Nov 2024 21:16:20 +0200 Subject: [PATCH 19/25] added calculator new specs; fixed controller; --- .../account/calculators_controller.rb | 9 +++- spec/factories/calculators.rb | 3 +- spec/factories/fields.rb | 2 +- spec/requests/account/calculators_spec.rb | 51 +++++++++++++++++++ 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/app/controllers/account/calculators_controller.rb b/app/controllers/account/calculators_controller.rb index dec87775d..4a1cd4b6a 100644 --- a/app/controllers/account/calculators_controller.rb +++ b/app/controllers/account/calculators_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Account::CalculatorsController < Account::BaseController - before_action :resource, only: [:edit, :update, :destroy] load_and_authorize_resource def index @@ -23,6 +22,8 @@ def new end def edit + @calculator = resource + collect_fields_for_form end @@ -37,6 +38,8 @@ def create end def update + @calculator = resource + if updater redirect_to edit_account_calculator_path(slug: @calculator), notice: t("notifications.calculator_updated") else @@ -47,6 +50,8 @@ def update end def destroy + @calculator = resource + @calculator.destroy redirect_to account_calculators_path, notice: t("notifications.calculator_deleted"), status: :see_other @@ -59,7 +64,7 @@ def collection end def resource - Calculator.find(params[:slug]) + collection.friendly.find(params[:slug]) end def collect_fields_for_form diff --git a/spec/factories/calculators.rb b/spec/factories/calculators.rb index 76607bb4b..8f6db7dc2 100644 --- a/spec/factories/calculators.rb +++ b/spec/factories/calculators.rb @@ -18,7 +18,6 @@ FactoryBot.define do factory :calculator do en_name { "English Calculator" } - uk_name { "Український калькуялтор" } - slug { "calculator" } + uk_name { "Український калькулятор" } end end diff --git a/spec/factories/fields.rb b/spec/factories/fields.rb index 1f202204f..d8870a92c 100644 --- a/spec/factories/fields.rb +++ b/spec/factories/fields.rb @@ -19,7 +19,7 @@ # FactoryBot.define do factory :field do - kind { 0 } + kind { "number" } en_label { "Label" } uk_label { "Label" } var_name { "var" } diff --git a/spec/requests/account/calculators_spec.rb b/spec/requests/account/calculators_spec.rb index cb92c8b16..ad4123801 100644 --- a/spec/requests/account/calculators_spec.rb +++ b/spec/requests/account/calculators_spec.rb @@ -7,6 +7,10 @@ let!(:calculator) { create(:calculator) } + let(:user) { create(:user) } + let(:locale) { "en" } + let(:new_path) { new_account_calculator_path(locale: locale) } + describe "DELETE #destroy" do it "destroys the calculator and redirects" do expect do @@ -40,4 +44,51 @@ end end end + + describe "GET #new" do + subject { get new_path } + + context "when the user is authorized" do + it "initializes a new Calculator object with fields and formulas" do + subject + + expect(response).to have_http_status(:ok) + + calculator = assigns(:calculator) + expect(calculator).to be_a_new(Calculator) + + expect(calculator.fields.size).to eq(1) + expect(calculator.formulas.size).to eq(1) + + expect(calculator.fields.first).to be_a_new(Field) + expect(calculator.formulas.first).to be_a_new(Formula) + end + + it "renders the new template" do + subject + expect(response).to render_template(:new) + end + end + + context "when the locale is different" do + let(:locale) { "uk" } + + it "handles the locale correctly" do + subject + expect(I18n.locale).to eq(:uk) + expect(response).to have_http_status(:ok) + end + end + + context "when the user is not logged in" do + before do + sign_out(:user) + end + + it "redirects to the login page" do + subject + expect(response).to redirect_to(new_user_session_path) + end + end + end end From 123bc949c02a2df5100628d6bfc2042d3e603b7c Mon Sep 17 00:00:00 2001 From: Andrii Date: Wed, 20 Nov 2024 09:30:59 +0000 Subject: [PATCH 20/25] add key for staging --- .github/workflows/ci.yml | 3 ++- config/credentials/staging_deploy_id_ed25519_enc | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 config/credentials/staging_deploy_id_ed25519_enc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 315493d3a..6b7191454 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,7 @@ jobs: json-path: tmp/rspec_results.json if: always() - deploy-to-develop: + deploy-to-staging: needs: rspec runs-on: ubuntu-latest if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true @@ -107,6 +107,7 @@ jobs: with: target: development deploy_key: ${{ secrets.DEV_DEPLOY_KEY }} + enc_rsa_key_pth: config/credentials/deploy_id_ed25519_enc deploy-to-production: needs: rspec diff --git a/config/credentials/staging_deploy_id_ed25519_enc b/config/credentials/staging_deploy_id_ed25519_enc new file mode 100644 index 000000000..bd3eb4dc7 --- /dev/null +++ b/config/credentials/staging_deploy_id_ed25519_enc @@ -0,0 +1,10 @@ +U2FsdGVkX1+dAao58N25fwh/ICC4DEgTajI1FHPj36c7HRFb0WUo9mDljBWNEqA7 +R0+5nXn3pROcIFNjSu6wInKiCXB6wHTN+N29k+I6tG5HDkSElt7OZJffeluFfz/N +XKtGdPZuXjACLE343yCclrde9MJBZBsFVujONQfj4DMcrFkUmaPSGQyKnt+zahl7 +ML8Q2tnF8eFdFk9DyOqEha0F+Ux/+ZJtAjL0e8bYQQZ1Oz8vmqWgradu0V5WL37x +5mXo9BAEfb+oxOjOaN29eIFcTWGP6d0sXtfT3/815e6FKCphZ2v7A3eaKqb/s0Zi +LF+RKd+WMXGS2kB30ySZmJ6mX+uFbn2Qfq7lyy81qGwO1hfbAUvEqLe/YE5UaDDi +d8II5hh1SOitUbEU8pKjDPhuYdYZJggRa0WEC9P5rpC5JK750IPok4qFlc5UZp/B +pV+ey/roGY/HbbV9dZ0NIRoPZtWFjQvMOx+RhrCLmUvMyk4hv43IbLkMm3Z0sioZ +AktZWPns5yZfAYZYeVBFYbDvvuJHJiFlD35mzuDzOwh0p02nYi2pldn6P/1i8Rbf +tk0USJUvX/bDI5ooCEwyCw== \ No newline at end of file From 95ca3b156cf02dc0c54c0410d1be198f675b5195 Mon Sep 17 00:00:00 2001 From: Andrii Date: Wed, 20 Nov 2024 09:49:27 +0000 Subject: [PATCH 21/25] add updating packages --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b7191454..468aca764 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,9 @@ jobs: ruby-version: 3.3.5 bundler-cache: true + - name: Update packages + run: sudo apt-get update + - name: Install system dependencies run: sudo apt-get install -y libvips42 libvips-dev imagemagick From 033ff376f83ba3f18bc2d489564f2019b69573e1 Mon Sep 17 00:00:00 2001 From: Andrii Date: Wed, 20 Nov 2024 13:41:03 +0000 Subject: [PATCH 22/25] fix --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 468aca764..276c16bdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,8 +109,8 @@ jobs: - uses: miloserdow/capistrano-deploy@v3 with: target: development - deploy_key: ${{ secrets.DEV_DEPLOY_KEY }} - enc_rsa_key_pth: config/credentials/deploy_id_ed25519_enc + deploy_key: ${{ secrets.STAGING_KEY_PASSWORD }} + enc_rsa_key_pth: config/credentials/staging_deploy_id_ed25519_enc deploy-to-production: needs: rspec From 0a64360fbadb581ad022c6aae11a98bb7dcc4066 Mon Sep 17 00:00:00 2001 From: Ivan Marynych <49816584+loqimean@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:14:19 +0200 Subject: [PATCH 23/25] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 276c16bdc..05b3114a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,7 +108,7 @@ jobs: - uses: miloserdow/capistrano-deploy@v3 with: - target: development + target: staging deploy_key: ${{ secrets.STAGING_KEY_PASSWORD }} enc_rsa_key_pth: config/credentials/staging_deploy_id_ed25519_enc From 3cb4be9caecafbd52465b23b9dccf3ad138a0bb0 Mon Sep 17 00:00:00 2001 From: Ivan Marynych <49816584+loqimean@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:33:55 +0200 Subject: [PATCH 24/25] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1bb70b38..0f6a65d4f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ The latest version from the release branch 'master' is automatically deployed to - Bootstrap ## Clone - + $ `git clone https://github.com/ita-social-projects/ZeroWaste.git` ## Local setup From a6b220878a482514022cc5ba7fe2e72b1e5755e2 Mon Sep 17 00:00:00 2001 From: SleekMutt <84468904+SleekMutt@users.noreply.github.com> Date: Sun, 24 Nov 2024 12:46:54 +0200 Subject: [PATCH 25/25] Added flipper, so admin could enable or disable mhc calculator (#969) * Added flipper, so admin could enable or disable mhc calculator --- app/controllers/calculators_controller.rb | 7 +++++++ app/views/account/shared/_navigation.html.erb | 5 +++++ config/initializers/flipper/features.rb | 3 +++ config/locales/en/en.yml | 3 +++ config/locales/uk/uk.yml | 3 +++ spec/requests/calculators_spec.rb | 21 ++++++++++++++----- spec/support/shared/context/feature_flags.rb | 14 +++++++++++++ 7 files changed, 51 insertions(+), 5 deletions(-) diff --git a/app/controllers/calculators_controller.rb b/app/controllers/calculators_controller.rb index e0b7a4460..6693a80ad 100644 --- a/app/controllers/calculators_controller.rb +++ b/app/controllers/calculators_controller.rb @@ -2,6 +2,7 @@ class CalculatorsController < ApplicationController before_action :authenticate_user!, only: :receive_recomendations + before_action :check_mhc_flipper, only: :mhc_calculator def index if Flipper[:show_calculators_list].enabled? @@ -52,4 +53,10 @@ def collection def resource collection.friendly.find(params[:slug]) end + + def check_mhc_flipper + return if Flipper[:mhc_calculator_status].enabled? + + raise ActionController::RoutingError, "Mhc calculator flipper is disabled" + end end diff --git a/app/views/account/shared/_navigation.html.erb b/app/views/account/shared/_navigation.html.erb index 1df545e99..264fd2e55 100644 --- a/app/views/account/shared/_navigation.html.erb +++ b/app/views/account/shared/_navigation.html.erb @@ -10,6 +10,11 @@ <%= link_to t("layouts.navigation.calculate"), calculator_path, class: "tab-main-page" %> <% end %> +
  • + <% if Flipper[:mhc_calculator_status].enabled? %> + <%= link_to t("layouts.navigation.mhc_calculator"), mhc_calculator_path, class: "tab-main-page" %> + <% end %> +
  • <%= link_to t("layouts.navigation.about_us"), about_path, class: "tab-main-page" %>
  • diff --git a/config/initializers/flipper/features.rb b/config/initializers/flipper/features.rb index 24f9571ad..93387a7eb 100644 --- a/config/initializers/flipper/features.rb +++ b/config/initializers/flipper/features.rb @@ -9,4 +9,7 @@ Flipper[:sandbox_mode].en_description = "This feature flag is responsible for enabling sandbox mode" Flipper[:sandbox_mode].uk_description = "Відкриває можливість використовувати режим пісочниці" + + Flipper[:mhc_calculator_status].en_description = "This feature flag is responsible for enabling mhc calculator" + Flipper[:mhc_calculator_status].uk_description = "Відкриває можливість використовувати калькулятор продуктів жіночої гігієни" end diff --git a/config/locales/en/en.yml b/config/locales/en/en.yml index 4a624b943..3bb164b80 100644 --- a/config/locales/en/en.yml +++ b/config/locales/en/en.yml @@ -126,6 +126,7 @@ en: about_us: "About us" admin: "Admin" calculate: "Diaper calculator" + mhc_calculator: "Pad calculator" log_out: "Log Out" sign_up: "Sign Up" log_in: "Log In" @@ -480,6 +481,8 @@ en: name: "Show calculators on the public side" sandbox_mode: name: "Enable sandbox mode" + mhc_calculator_status: + name: "Enable mhc calculator" sessions: new: log_in_header: "Log In" diff --git a/config/locales/uk/uk.yml b/config/locales/uk/uk.yml index 0b52f53aa..8ba07e394 100644 --- a/config/locales/uk/uk.yml +++ b/config/locales/uk/uk.yml @@ -109,6 +109,7 @@ uk: admin: "Адміністратор" calculate: "Калькулятор підгузків" log_out: "Вийти" + mhc_calculator: "Калькулятор прокладок" sign_up: "Зареєструватися" log_in: "Увійти" contact_us: "Зв'яжіться з нами" @@ -440,6 +441,8 @@ uk: name: "Показати калькулятори користувачам" sandbox_mode: name: "Увімкнути режим пісочниці" + mhc_calculator_status: + name: "Увімкнути калькулятор продуктів жіночої гігієни" sessions: new: log_in_header: "Увійти" diff --git a/spec/requests/calculators_spec.rb b/spec/requests/calculators_spec.rb index 2be0577e0..df316e5ba 100644 --- a/spec/requests/calculators_spec.rb +++ b/spec/requests/calculators_spec.rb @@ -111,12 +111,23 @@ end describe "GET /mhc_calculator" do - it "renders pad calculator" do - get mhc_calculator_path + context "mhc calculator is enabled" do + include_context :mhc_calculator_enabled - expect(response).to be_successful - expect(response).to render_template(:mhc_calculator) - expect(response.body).to include("results") + it "renders pad calculator" do + get mhc_calculator_path + + expect(response).to be_successful + expect(response).to render_template(:mhc_calculator) + end + end + + context "mhc calculator is disabled" do + include_context :mhc_calculator_disabled + + it "renders pad calculator" do + expect { get mhc_calculator_path }.to raise_error(ActionController::RoutingError) + end end end diff --git a/spec/support/shared/context/feature_flags.rb b/spec/support/shared/context/feature_flags.rb index a037cdaa9..cb62bed93 100644 --- a/spec/support/shared/context/feature_flags.rb +++ b/spec/support/shared/context/feature_flags.rb @@ -35,5 +35,19 @@ def sandbox_mode_context(mode) end end +RSpec.shared_context :mhc_calculator_enabled do + before do + FeatureFlag.find_or_create_by!(name: "show_calculators_list") + Flipper.enable(:mhc_calculator_status) + end +end + +RSpec.shared_context :mhc_calculator_disabled do + before do + FeatureFlag.find_or_create_by!(name: "show_calculators_list") + Flipper.disable(:mhc_calculator_status) + end +end + sandbox_mode_context(:enable) sandbox_mode_context(:disable)